refactor: metrics review changes (#94)

Signed-off-by: izzy <me@insrt.uk>
This commit is contained in:
Paul Makles
2026-05-14 13:05:12 +01:00
committed by GitHub
parent aac91631a9
commit a20f02fcda
12 changed files with 120 additions and 18 deletions
+27 -4
View File
@@ -95,12 +95,35 @@
]
}
},
"/api/metrics/submit/{id}/backup/end": {
"/api/metrics/submit/{repositoryId}/backup/start": {
"post": {
"operationId": "submitMetricBackupStart",
"parameters": [
{
"name": "repositoryId",
"required": true,
"in": "path",
"schema": {
"type": "string"
}
}
],
"responses": {
"204": {
"description": ""
}
},
"tags": [
"metrics"
]
}
},
"/api/metrics/submit/{repositoryId}/backup/end": {
"post": {
"operationId": "submitMetricBackupEnd",
"parameters": [
{
"name": "id",
"name": "repositoryId",
"required": true,
"in": "path",
"schema": {
@@ -128,12 +151,12 @@
]
}
},
"/api/metrics/submit/{id}/size": {
"/api/metrics/submit/{repositoryId}/size": {
"patch": {
"operationId": "submitMetricRepositorySize",
"parameters": [
{
"name": "id",
"name": "repositoryId",
"required": true,
"in": "path",
"schema": {
+10 -4
View File
@@ -93,15 +93,21 @@ export function oidcDeviceFlow(opts?: Oazapfts.RequestOpts) {
...opts
}));
}
export function submitMetricBackupEnd(id: string, submitBackupEndRequestDto: SubmitBackupEndRequestDto, opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchText(`/api/metrics/submit/${encodeURIComponent(id)}/backup/end`, oazapfts.json({
export function submitMetricBackupStart(repositoryId: string, opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchText(`/api/metrics/submit/${encodeURIComponent(repositoryId)}/backup/start`, {
...opts,
method: "POST"
}));
}
export function submitMetricBackupEnd(repositoryId: string, submitBackupEndRequestDto: SubmitBackupEndRequestDto, opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchText(`/api/metrics/submit/${encodeURIComponent(repositoryId)}/backup/end`, oazapfts.json({
...opts,
method: "POST",
body: submitBackupEndRequestDto
})));
}
export function submitMetricRepositorySize(id: string, submitUpdateSizeRequestDto: SubmitUpdateSizeRequestDto, opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchText(`/api/metrics/submit/${encodeURIComponent(id)}/size`, oazapfts.json({
export function submitMetricRepositorySize(repositoryId: string, submitUpdateSizeRequestDto: SubmitUpdateSizeRequestDto, opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchText(`/api/metrics/submit/${encodeURIComponent(repositoryId)}/size`, oazapfts.json({
...opts,
method: "PATCH",
body: submitUpdateSizeRequestDto
@@ -10,25 +10,32 @@ import { MetricsService } from 'src/services/metrics.service';
export class MetricsController {
constructor(private readonly metrics: MetricsService) {}
@Post('/:id/backup/end')
@Post('/:repositoryId/backup/start')
@AuthRoute()
@HttpCode(HttpStatus.NO_CONTENT)
submitMetricBackupStart(@Auth() auth: AuthDto, @Param('repositoryId') repositoryId: string): Promise<void> {
return this.metrics.submitBackupStart(auth, repositoryId);
}
@Post('/:repositoryId/backup/end')
@AuthRoute()
@HttpCode(HttpStatus.NO_CONTENT)
submitMetricBackupEnd(
@Auth() auth: AuthDto,
@Param('id') id: string,
@Param('repositoryId') repositoryId: string,
@Body() dto: SubmitBackupEndRequestDto,
): Promise<void> {
return this.metrics.submitBackupEnd(auth, id, dto);
return this.metrics.submitBackupEnd(auth, repositoryId, dto);
}
@Patch('/:id/size')
@Patch('/:repositoryId/size')
@AuthRoute()
@HttpCode(HttpStatus.NO_CONTENT)
submitMetricRepositorySize(
@Auth() auth: AuthDto,
@Param('id') id: string,
@Param('repositoryId') repositoryId: string,
@Body() dto: SubmitUpdateSizeRequestDto,
): Promise<void> {
return this.metrics.submitRepositorySize(auth, id, dto);
return this.metrics.submitRepositorySize(auth, repositoryId, dto);
}
}
@@ -0,0 +1,9 @@
import { Kysely, sql } from 'kysely';
export async function up(db: Kysely<any>): Promise<void> {
await sql`ALTER TABLE "repositoryMetrics" ADD "lastStarted" timestamp with time zone;`.execute(db);
}
export async function down(db: Kysely<any>): Promise<void> {
await sql`ALTER TABLE "repositoryMetrics" DROP COLUMN "lastStarted";`.execute(db);
}
@@ -9,6 +9,9 @@ export class RepositoryMetricsTable {
@Column({ type: 'bigint' })
sizeBytes!: number;
@Column({ type: 'timestamp with time zone', nullable: true })
lastStarted?: Date;
@Column({ type: 'timestamp with time zone', nullable: true })
lastBackup?: Date;
@@ -0,0 +1,25 @@
import { Column, ForeignKeyColumn, Table } from '@immich/sql-tools';
import { RepositoryTable } from './repository.table';
@Table({ name: 'repositoryMetricsHistory' })
export class RepositoryMetricsTable {
id!: string;
@ForeignKeyColumn(() => RepositoryTable, { primary: true, onUpdate: 'CASCADE', onDelete: 'CASCADE' })
repositoryId!: string;
@Column({ type: 'bigint', nullable: false })
sizeBytes?: number;
@Column({ type: 'timestamp with time zone', nullable: true })
started?: Date;
@Column({ type: 'timestamp with time zone', nullable: true })
backup?: Date;
@Column({ type: 'timestamp with time zone', nullable: true })
successfulBackup?: Date;
@Column({ type: 'integer', nullable: true })
backupDuration?: number;
}
@@ -11,6 +11,17 @@ export class MetricsService {
private readonly metrics: RepositoryMetricsRepository,
) {}
async submitBackupStart(auth: AuthDto, repositoryId: string) {
const repository = await this.repositories.get(repositoryId);
if (repository.userId !== auth.id) {
throw new UnauthorizedException();
}
await this.metrics.save(repositoryId, {
lastStarted: new Date(),
});
}
async submitBackupEnd(auth: AuthDto, repositoryId: string, dto: SubmitBackupEndRequestDto) {
const repository = await this.repositories.get(repositoryId);
if (repository.userId !== auth.id) {
@@ -25,6 +25,7 @@ export abstract class Backend {
abstract getResticEndpoint(id: string): Promise<string>;
abstract submitMetricBackupStart(id: string): Promise<void>;
abstract submitMetricBackupEnd(id: string, success: boolean, duration: number): Promise<void>;
abstract submitMetricRepositorySize(id: string, size: number): Promise<void>;
@@ -102,6 +102,10 @@ export class LocalBackend extends Backend {
return resolve(this.configuration.path, id);
}
submitMetricBackupStart(): Promise<void> {
throw new Error('not capable');
}
submitMetricBackupEnd(): Promise<void> {
throw new Error('not capable');
}
@@ -52,6 +52,10 @@ export class S3Backend extends Backend {
return `s3:${this.configuration.endpoint}/${id}`;
}
submitMetricBackupStart(): Promise<void> {
throw new Error('not capable');
}
submitMetricBackupEnd(): Promise<void> {
throw new Error('not capable');
}
@@ -8,6 +8,7 @@ import {
RepositoryCreateRequestDto,
RepositoryUpdateRequestDto,
submitMetricBackupEnd,
submitMetricBackupStart,
submitMetricRepositorySize,
updateRepository,
} from 'yucca-api-client';
@@ -66,6 +67,10 @@ export class YuccaBackend extends Backend {
return url;
}
submitMetricBackupStart(id: string): Promise<void> {
return submitMetricBackupStart(id, this.requestOptions);
}
submitMetricBackupEnd(id: string, success: boolean, durationMs: number): Promise<void> {
return submitMetricBackupEnd(
id,
@@ -388,6 +388,10 @@ export class RepositoryService {
const { endpoint, key } = await this.getResticParameters(id);
const { backendId } = await this.repository.get(id);
const { configuration } = await this.backend.getBackend(backendId);
const backend = Backend.from(configuration, this.moduleConfig.get());
return new Promise((resolve) => {
const startTime = Date.now();
@@ -401,6 +405,10 @@ export class RepositoryService {
logId,
});
if (backend.isMetricsCapable()) {
await backend.submitMetricBackupStart(id);
}
try {
const taskSignal = this.tasks.startTask(id, TaskType.Backup, logId, signal);
await this.restic.backup(endpoint, key, paths, log, taskSignal);
@@ -428,10 +436,6 @@ export class RepositoryService {
complete();
}
const { backendId } = await this.repository.get(id);
const { configuration } = await this.backend.getBackend(backendId);
const backend = Backend.from(configuration, this.moduleConfig.get());
if (backend.isMetricsCapable()) {
await backend.submitMetricBackupEnd(id, !error, lastBackupDuration);
}