mirror of
https://github.com/immich-app/yucca.git
synced 2026-09-30 13:33:00 +08:00
refactor: metrics review changes (#94)
Signed-off-by: izzy <me@insrt.uk>
This commit is contained in:
@@ -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": {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user