feat(upgrade): expose twenty_upgrade_workspaces_up_to_date_total (#20555)
## Summary Adds a fourth gauge alongside the existing `twenty_upgrade_workspaces_behind_total` / `twenty_upgrade_workspaces_failed_total` so dashboards can show how many workspaces are currently healthy, not just the ones that need attention. - New gauge: `twenty_upgrade_workspaces_up_to_date_total` - New count is computed during `UpgradeStatusService.refreshInstanceAndAllWorkspacesStatus` (cheap — we already iterate over every workspace), persisted in the existing `UpgradeStatusCacheService` so the cache-hit path stays a single round trip, and surfaced via `InstanceAndAllWorkspacesUpgradeStatusDTO` for the admin panel. ## Files - `packages/twenty-server/src/engine/core-modules/upgrade/upgrade-gauge.service.ts` — register the new ObservableGauge - `packages/twenty-server/src/engine/core-modules/upgrade/services/upgrade-status.service.ts` — count UP_TO_DATE workspaces during refresh, propagate through cached path - `packages/twenty-server/src/engine/core-modules/upgrade/services/upgrade-status-cache.service.ts` — persist `upToDateWorkspaceCount` next to behind/failed sets - `packages/twenty-server/src/engine/core-modules/upgrade/dtos/instance-and-all-workspaces-upgrade-status.dto.ts` — `Int` field on the admin DTO - Tests: extended `upgrade-status.service.spec.ts` (14/14 green) — cached and refresh paths both assert on `upToDateWorkspaceCount` ## Follow-up A companion `twenty-infra` PR adds the new tile + line on the upgrade-status Grafana dashboard.
This commit is contained in:
@@ -294,6 +294,7 @@ export type InstanceAndAllWorkspacesUpgradeStatus = {
|
||||
__typename?: 'InstanceAndAllWorkspacesUpgradeStatus';
|
||||
computedAt: Scalars['DateTime'];
|
||||
instanceUpgradeStatus: InstanceUpgradeStatus;
|
||||
upToDateWorkspaceCount: Scalars['Int'];
|
||||
workspacesBehind: Array<WorkspaceUpgradeRef>;
|
||||
workspacesFailed: Array<WorkspaceUpgradeRef>;
|
||||
};
|
||||
|
||||
+4
-1
@@ -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;
|
||||
}
|
||||
|
||||
+7
@@ -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),
|
||||
});
|
||||
});
|
||||
|
||||
+15
@@ -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<number> {
|
||||
const raw = await this.cacheStorage.get<number>(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<void> {
|
||||
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),
|
||||
]);
|
||||
}
|
||||
|
||||
+18
-6
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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<InstanceAndAllWorkspacesUpgradeStatus | null> {
|
||||
|
||||
Reference in New Issue
Block a user