diff --git a/packages/twenty-front/src/generated-admin/graphql.ts b/packages/twenty-front/src/generated-admin/graphql.ts index 1839826d25..24e6ddca53 100644 --- a/packages/twenty-front/src/generated-admin/graphql.ts +++ b/packages/twenty-front/src/generated-admin/graphql.ts @@ -294,6 +294,7 @@ export type InstanceAndAllWorkspacesUpgradeStatus = { __typename?: 'InstanceAndAllWorkspacesUpgradeStatus'; computedAt: Scalars['DateTime']; instanceUpgradeStatus: InstanceUpgradeStatus; + upToDateWorkspaceCount: Scalars['Int']; workspacesBehind: Array; workspacesFailed: Array; }; diff --git a/packages/twenty-server/src/engine/core-modules/upgrade/dtos/instance-and-all-workspaces-upgrade-status.dto.ts b/packages/twenty-server/src/engine/core-modules/upgrade/dtos/instance-and-all-workspaces-upgrade-status.dto.ts index 9433f283e0..1e95218e27 100644 --- a/packages/twenty-server/src/engine/core-modules/upgrade/dtos/instance-and-all-workspaces-upgrade-status.dto.ts +++ b/packages/twenty-server/src/engine/core-modules/upgrade/dtos/instance-and-all-workspaces-upgrade-status.dto.ts @@ -1,4 +1,4 @@ -import { Field, ObjectType } from '@nestjs/graphql'; +import { Field, Int, ObjectType } from '@nestjs/graphql'; import { InstanceUpgradeStatusDTO } from 'src/engine/core-modules/upgrade/dtos/instance-upgrade-status.dto'; import { WorkspaceUpgradeRefDTO } from 'src/engine/core-modules/upgrade/dtos/workspace-upgrade-ref.dto'; @@ -14,6 +14,9 @@ export class InstanceAndAllWorkspacesUpgradeStatusDTO { @Field(() => [WorkspaceUpgradeRefDTO]) workspacesFailed: WorkspaceUpgradeRefDTO[]; + @Field(() => Int) + upToDateWorkspaceCount: number; + @Field(() => Date) computedAt: Date; } diff --git a/packages/twenty-server/src/engine/core-modules/upgrade/services/__tests__/upgrade-status.service.spec.ts b/packages/twenty-server/src/engine/core-modules/upgrade/services/__tests__/upgrade-status.service.spec.ts index 5d27ab1bb4..0bf28a9fbb 100644 --- a/packages/twenty-server/src/engine/core-modules/upgrade/services/__tests__/upgrade-status.service.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/upgrade/services/__tests__/upgrade-status.service.spec.ts @@ -57,6 +57,7 @@ describe('UpgradeStatusService', () => { let cacheGetComputedAt: jest.Mock; let cacheGetBehindWorkspaceIds: jest.Mock; let cacheGetFailedWorkspaceIds: jest.Mock; + let cacheGetUpToDateWorkspaceCount: jest.Mock; let cacheWrite: jest.Mock; let cacheInvalidate: jest.Mock; @@ -81,6 +82,7 @@ describe('UpgradeStatusService', () => { cacheGetComputedAt = jest.fn(); cacheGetBehindWorkspaceIds = jest.fn().mockResolvedValue([]); cacheGetFailedWorkspaceIds = jest.fn().mockResolvedValue([]); + cacheGetUpToDateWorkspaceCount = jest.fn().mockResolvedValue(0); cacheWrite = jest.fn().mockResolvedValue(undefined); cacheInvalidate = jest.fn().mockResolvedValue(undefined); @@ -115,6 +117,7 @@ describe('UpgradeStatusService', () => { getComputedAt: cacheGetComputedAt, getBehindWorkspaceIds: cacheGetBehindWorkspaceIds, getFailedWorkspaceIds: cacheGetFailedWorkspaceIds, + getUpToDateWorkspaceCount: cacheGetUpToDateWorkspaceCount, write: cacheWrite, invalidate: cacheInvalidate, }, @@ -277,6 +280,7 @@ describe('UpgradeStatusService', () => { cacheGetComputedAt.mockResolvedValue(computedAt); cacheGetBehindWorkspaceIds.mockResolvedValue(['ws-2']); cacheGetFailedWorkspaceIds.mockResolvedValue(['ws-3']); + cacheGetUpToDateWorkspaceCount.mockResolvedValue(5); getLastAttemptedInstanceCommand.mockResolvedValue({ name: LAST_INSTANCE_COMMAND, status: 'completed', @@ -295,6 +299,7 @@ describe('UpgradeStatusService', () => { expect(result.workspacesBehind).toEqual([{ id: 'ws-2', name: 'Banana' }]); expect(result.workspacesFailed).toEqual([{ id: 'ws-3', name: 'Cherry' }]); + expect(result.upToDateWorkspaceCount).toBe(5); expect(result.computedAt).toEqual(computedAt); expect(getWorkspaceLastAttemptedCommandName).not.toHaveBeenCalled(); expect(cacheWrite).not.toHaveBeenCalled(); @@ -385,10 +390,12 @@ describe('UpgradeStatusService', () => { expect(result.workspacesBehind).toEqual([{ id: 'ws-2', name: 'Banana' }]); expect(result.workspacesFailed).toEqual([{ id: 'ws-3', name: 'Cherry' }]); + expect(result.upToDateWorkspaceCount).toBe(1); expect(cacheWrite).toHaveBeenCalledWith({ behindWorkspaceIds: ['ws-2'], failedWorkspaceIds: ['ws-3'], + upToDateWorkspaceCount: 1, computedAt: expect.any(Date), }); }); diff --git a/packages/twenty-server/src/engine/core-modules/upgrade/services/upgrade-status-cache.service.ts b/packages/twenty-server/src/engine/core-modules/upgrade/services/upgrade-status-cache.service.ts index 399461ac52..ade3db3c2b 100644 --- a/packages/twenty-server/src/engine/core-modules/upgrade/services/upgrade-status-cache.service.ts +++ b/packages/twenty-server/src/engine/core-modules/upgrade/services/upgrade-status-cache.service.ts @@ -8,6 +8,7 @@ import { CacheStorageNamespace } from 'src/engine/core-modules/cache-storage/typ const BEHIND_IDS_KEY = 'upgrade-status:behind-workspace-ids'; const FAILED_IDS_KEY = 'upgrade-status:failed-workspace-ids'; +const UP_TO_DATE_COUNT_KEY = 'upgrade-status:up-to-date-workspace-count'; const COMPUTED_AT_KEY = 'upgrade-status:computed-at'; const CACHE_TTL_MS = 60 * 60 * 1000; @@ -32,13 +33,21 @@ export class UpgradeStatusCacheService { return this.cacheStorage.setMembers(FAILED_IDS_KEY); } + async getUpToDateWorkspaceCount(): Promise { + const raw = await this.cacheStorage.get(UP_TO_DATE_COUNT_KEY); + + return isDefined(raw) ? raw : 0; + } + async write({ behindWorkspaceIds, failedWorkspaceIds, + upToDateWorkspaceCount, computedAt, }: { behindWorkspaceIds: string[]; failedWorkspaceIds: string[]; + upToDateWorkspaceCount: number; computedAt: Date; }): Promise { await Promise.all([ @@ -57,6 +66,11 @@ export class UpgradeStatusCacheService { failedWorkspaceIds, CACHE_TTL_MS, ), + this.cacheStorage.set( + UP_TO_DATE_COUNT_KEY, + upToDateWorkspaceCount, + CACHE_TTL_MS, + ), this.cacheStorage.set( COMPUTED_AT_KEY, computedAt.toISOString(), @@ -69,6 +83,7 @@ export class UpgradeStatusCacheService { await Promise.all([ this.cacheStorage.del(BEHIND_IDS_KEY), this.cacheStorage.del(FAILED_IDS_KEY), + this.cacheStorage.del(UP_TO_DATE_COUNT_KEY), this.cacheStorage.del(COMPUTED_AT_KEY), ]); } diff --git a/packages/twenty-server/src/engine/core-modules/upgrade/services/upgrade-status.service.ts b/packages/twenty-server/src/engine/core-modules/upgrade/services/upgrade-status.service.ts index 695c8e141f..77fe473262 100644 --- a/packages/twenty-server/src/engine/core-modules/upgrade/services/upgrade-status.service.ts +++ b/packages/twenty-server/src/engine/core-modules/upgrade/services/upgrade-status.service.ts @@ -45,6 +45,7 @@ export type InstanceAndAllWorkspacesUpgradeStatus = { instanceUpgradeStatus: InstanceUpgradeStatus; workspacesBehind: WorkspaceUpgradeRef[]; workspacesFailed: WorkspaceUpgradeRef[]; + upToDateWorkspaceCount: number; computedAt: Date; }; @@ -144,12 +145,17 @@ export class UpgradeStatusService { return this.refreshInstanceAndAllWorkspacesStatus(); } - const [instanceUpgradeStatus, behindWorkspaceIds, failedWorkspaceIds] = - await Promise.all([ - this.getInstanceStatus(), - this.upgradeStatusCacheService.getBehindWorkspaceIds(), - this.upgradeStatusCacheService.getFailedWorkspaceIds(), - ]); + const [ + instanceUpgradeStatus, + behindWorkspaceIds, + failedWorkspaceIds, + upToDateWorkspaceCount, + ] = await Promise.all([ + this.getInstanceStatus(), + this.upgradeStatusCacheService.getBehindWorkspaceIds(), + this.upgradeStatusCacheService.getFailedWorkspaceIds(), + this.upgradeStatusCacheService.getUpToDateWorkspaceCount(), + ]); const workspaceNamesById = await this.loadWorkspaceNamesById([ ...behindWorkspaceIds, @@ -166,6 +172,7 @@ export class UpgradeStatusService { failedWorkspaceIds, workspaceNamesById, ), + upToDateWorkspaceCount, computedAt, }; } @@ -180,6 +187,7 @@ export class UpgradeStatusService { const workspacesBehind: WorkspaceUpgradeRef[] = []; const workspacesFailed: WorkspaceUpgradeRef[] = []; + let upToDateWorkspaceCount = 0; for (const workspaceStatus of workspaceStatuses) { const workspaceRef: WorkspaceUpgradeRef = { @@ -191,6 +199,8 @@ export class UpgradeStatusService { workspacesBehind.push(workspaceRef); } else if (workspaceStatus.health === UpgradeHealthEnum.FAILED) { workspacesFailed.push(workspaceRef); + } else if (workspaceStatus.health === UpgradeHealthEnum.UP_TO_DATE) { + upToDateWorkspaceCount++; } } @@ -199,6 +209,7 @@ export class UpgradeStatusService { await this.upgradeStatusCacheService.write({ behindWorkspaceIds: workspacesBehind.map((workspace) => workspace.id), failedWorkspaceIds: workspacesFailed.map((workspace) => workspace.id), + upToDateWorkspaceCount, computedAt, }); @@ -206,6 +217,7 @@ export class UpgradeStatusService { instanceUpgradeStatus, workspacesBehind, workspacesFailed, + upToDateWorkspaceCount, computedAt, }; } diff --git a/packages/twenty-server/src/engine/core-modules/upgrade/upgrade-gauge.service.ts b/packages/twenty-server/src/engine/core-modules/upgrade/upgrade-gauge.service.ts index 78f8da74f0..3afae6bf49 100644 --- a/packages/twenty-server/src/engine/core-modules/upgrade/upgrade-gauge.service.ts +++ b/packages/twenty-server/src/engine/core-modules/upgrade/upgrade-gauge.service.ts @@ -79,6 +79,19 @@ export class UpgradeGaugeService implements OnModuleInit { }, cacheValue: true, }); + + this.metricsService.createObservableGauge({ + metricName: 'twenty_upgrade_workspaces_up_to_date_total', + options: { + description: 'Number of workspaces up-to-date on upgrade commands', + }, + callback: async () => { + const upgradeStatus = await this.getCachedUpgradeStatus(); + + return upgradeStatus?.upToDateWorkspaceCount ?? 0; + }, + cacheValue: true, + }); } private async getCachedUpgradeStatus(): Promise {