Admin panel: App health check (#10546)
closes https://github.com/twentyhq/core-team-issues/issues/441 whats new - - app health with the proposed format -- update, scratched this format in favor of a more concise and more intentional health check. Now we will check app health only based on pendingMigrations <del> ``` status: system: { nodeVersion }, overview: { totalWorkspaces, criticalWorkspaces, workspacesWithPendingMigrations, healthDistribution: { healthy, warning, critical, } }, problematicWorkspaces: [ { workspaceId, severity, pendingMigrations, issuesSummary: { structural, data, relationship } } ] ``` </del> - errorMessage and details seperation -- before we used to send error in details which made it difficult if we want both error and details on the front -- usecase >> suppose app health indicator is not healthy but still want to send details - stateHistory with timestamp -- this is something I introduced, not sure about this. Basically the thought process was to store the LastState of the details, incase of no connection or timeout errors. This is not yet used on the front, just the endpoint. But it could be used on the front too - name unifying https://discord.com/channels/1130383047699738754/1346454192776155156 - json tree https://discord.com/channels/1130383047699738754/1346458558048501760 - match figma design https://discord.com/channels/1130383047699738754/1346451659647094815 - fix the collapse/open styles in tables https://discord.com/channels/1130383047699738754/1346452051974160406 - shift eye icon to expanded container https://discord.com/channels/1130383047699738754/1346452282669010987 - use H2Title for title and description of env variables groups https://discord.com/channels/1130383047699738754/1346434955936530452
This commit is contained in:
@ -11,6 +11,7 @@ import { QueueMetricsTimeRange } from 'src/engine/core-modules/admin-panel/enums
|
||||
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
||||
import { HEALTH_ERROR_MESSAGES } from 'src/engine/core-modules/health/constants/health-error-messages.constants';
|
||||
import { HealthIndicatorId } from 'src/engine/core-modules/health/enums/health-indicator-id.enum';
|
||||
import { AppHealthIndicator } from 'src/engine/core-modules/health/indicators/app.health';
|
||||
import { ConnectedAccountHealth } from 'src/engine/core-modules/health/indicators/connected-account.health';
|
||||
import { DatabaseHealthIndicator } from 'src/engine/core-modules/health/indicators/database.health';
|
||||
import { RedisHealthIndicator } from 'src/engine/core-modules/health/indicators/redis.health';
|
||||
@ -26,6 +27,7 @@ describe('AdminPanelHealthService', () => {
|
||||
let redisHealth: jest.Mocked<RedisHealthIndicator>;
|
||||
let workerHealth: jest.Mocked<WorkerHealthIndicator>;
|
||||
let connectedAccountHealth: jest.Mocked<ConnectedAccountHealth>;
|
||||
let appHealth: jest.Mocked<AppHealthIndicator>;
|
||||
let redisClient: jest.Mocked<RedisClientService>;
|
||||
let environmentService: jest.Mocked<EnvironmentService>;
|
||||
let loggerSpy: jest.SpyInstance;
|
||||
@ -35,6 +37,7 @@ describe('AdminPanelHealthService', () => {
|
||||
redisHealth = { isHealthy: jest.fn() } as any;
|
||||
workerHealth = { isHealthy: jest.fn(), getQueueDetails: jest.fn() } as any;
|
||||
connectedAccountHealth = { isHealthy: jest.fn() } as any;
|
||||
appHealth = { isHealthy: jest.fn() } as any;
|
||||
redisClient = {
|
||||
getClient: jest.fn().mockReturnValue({} as Redis),
|
||||
} as any;
|
||||
@ -53,6 +56,7 @@ describe('AdminPanelHealthService', () => {
|
||||
{ provide: RedisHealthIndicator, useValue: redisHealth },
|
||||
{ provide: WorkerHealthIndicator, useValue: workerHealth },
|
||||
{ provide: ConnectedAccountHealth, useValue: connectedAccountHealth },
|
||||
{ provide: AppHealthIndicator, useValue: appHealth },
|
||||
{ provide: RedisClientService, useValue: redisClient },
|
||||
{ provide: EnvironmentService, useValue: environmentService },
|
||||
],
|
||||
@ -108,6 +112,32 @@ describe('AdminPanelHealthService', () => {
|
||||
details: 'Account sync is operational',
|
||||
},
|
||||
});
|
||||
appHealth.isHealthy.mockResolvedValue({
|
||||
app: {
|
||||
status: 'up',
|
||||
details: {
|
||||
system: {
|
||||
nodeVersion: '16.0',
|
||||
},
|
||||
workspaces: {
|
||||
totalWorkspaces: 1,
|
||||
healthStatus: [
|
||||
{
|
||||
workspaceId: '1',
|
||||
summary: {
|
||||
structuralIssues: 0,
|
||||
dataIssues: 0,
|
||||
relationshipIssues: 0,
|
||||
pendingMigrations: 0,
|
||||
},
|
||||
severity: 'healthy',
|
||||
details: {},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const result = await service.getSystemHealthStatus();
|
||||
|
||||
@ -129,6 +159,10 @@ describe('AdminPanelHealthService', () => {
|
||||
...HEALTH_INDICATORS[HealthIndicatorId.connectedAccount],
|
||||
status: AdminPanelHealthServiceStatus.OPERATIONAL,
|
||||
},
|
||||
{
|
||||
...HEALTH_INDICATORS[HealthIndicatorId.app],
|
||||
status: AdminPanelHealthServiceStatus.OPERATIONAL,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@ -148,6 +182,9 @@ describe('AdminPanelHealthService', () => {
|
||||
connectedAccountHealth.isHealthy.mockResolvedValue({
|
||||
connectedAccount: { status: 'up' },
|
||||
});
|
||||
appHealth.isHealthy.mockResolvedValue({
|
||||
app: { status: 'down', details: {} },
|
||||
});
|
||||
|
||||
const result = await service.getSystemHealthStatus();
|
||||
|
||||
@ -169,6 +206,10 @@ describe('AdminPanelHealthService', () => {
|
||||
...HEALTH_INDICATORS[HealthIndicatorId.connectedAccount],
|
||||
status: AdminPanelHealthServiceStatus.OPERATIONAL,
|
||||
},
|
||||
{
|
||||
...HEALTH_INDICATORS[HealthIndicatorId.app],
|
||||
status: AdminPanelHealthServiceStatus.OUTAGE,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
@ -186,6 +227,9 @@ describe('AdminPanelHealthService', () => {
|
||||
connectedAccountHealth.isHealthy.mockRejectedValue(
|
||||
new Error(HEALTH_ERROR_MESSAGES.MESSAGE_SYNC_CHECK_FAILED),
|
||||
);
|
||||
appHealth.isHealthy.mockRejectedValue(
|
||||
new Error(HEALTH_ERROR_MESSAGES.APP_HEALTH_CHECK_FAILED),
|
||||
);
|
||||
|
||||
const result = await service.getSystemHealthStatus();
|
||||
|
||||
@ -207,6 +251,10 @@ describe('AdminPanelHealthService', () => {
|
||||
...HEALTH_INDICATORS[HealthIndicatorId.connectedAccount],
|
||||
status: AdminPanelHealthServiceStatus.OUTAGE,
|
||||
},
|
||||
{
|
||||
...HEALTH_INDICATORS[HealthIndicatorId.app],
|
||||
status: AdminPanelHealthServiceStatus.OUTAGE,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
@ -233,7 +281,8 @@ describe('AdminPanelHealthService', () => {
|
||||
expect(result).toStrictEqual({
|
||||
...HEALTH_INDICATORS[HealthIndicatorId.database],
|
||||
status: AdminPanelHealthServiceStatus.OPERATIONAL,
|
||||
details: JSON.stringify(details),
|
||||
details: JSON.stringify({ details }),
|
||||
errorMessage: undefined,
|
||||
queues: undefined,
|
||||
});
|
||||
});
|
||||
@ -266,7 +315,8 @@ describe('AdminPanelHealthService', () => {
|
||||
expect(result).toStrictEqual({
|
||||
...HEALTH_INDICATORS[HealthIndicatorId.worker],
|
||||
status: AdminPanelHealthServiceStatus.OPERATIONAL,
|
||||
details: undefined,
|
||||
details: JSON.stringify({ queues: mockQueues }),
|
||||
errorMessage: undefined,
|
||||
queues: mockQueues.map((queue) => ({
|
||||
id: `worker-${queue.queueName}`,
|
||||
queueName: queue.queueName,
|
||||
@ -290,7 +340,8 @@ describe('AdminPanelHealthService', () => {
|
||||
expect(result).toStrictEqual({
|
||||
...HEALTH_INDICATORS[HealthIndicatorId.redis],
|
||||
status: AdminPanelHealthServiceStatus.OUTAGE,
|
||||
details: HEALTH_ERROR_MESSAGES.REDIS_CONNECTION_FAILED,
|
||||
details: undefined,
|
||||
errorMessage: HEALTH_ERROR_MESSAGES.REDIS_CONNECTION_FAILED,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@ -10,6 +10,7 @@ import { SystemHealth } from 'src/engine/core-modules/admin-panel/dtos/system-he
|
||||
import { AdminPanelHealthServiceStatus } from 'src/engine/core-modules/admin-panel/enums/admin-panel-health-service-status.enum';
|
||||
import { QueueMetricsTimeRange } from 'src/engine/core-modules/admin-panel/enums/queue-metrics-time-range.enum';
|
||||
import { HealthIndicatorId } from 'src/engine/core-modules/health/enums/health-indicator-id.enum';
|
||||
import { AppHealthIndicator } from 'src/engine/core-modules/health/indicators/app.health';
|
||||
import { ConnectedAccountHealth } from 'src/engine/core-modules/health/indicators/connected-account.health';
|
||||
import { DatabaseHealthIndicator } from 'src/engine/core-modules/health/indicators/database.health';
|
||||
import { RedisHealthIndicator } from 'src/engine/core-modules/health/indicators/redis.health';
|
||||
@ -27,6 +28,7 @@ export class AdminPanelHealthService {
|
||||
private readonly redisHealth: RedisHealthIndicator,
|
||||
private readonly workerHealth: WorkerHealthIndicator,
|
||||
private readonly connectedAccountHealth: ConnectedAccountHealth,
|
||||
private readonly appHealth: AppHealthIndicator,
|
||||
private readonly redisClient: RedisClientService,
|
||||
) {}
|
||||
|
||||
@ -35,6 +37,7 @@ export class AdminPanelHealthService {
|
||||
[HealthIndicatorId.redis]: this.redisHealth,
|
||||
[HealthIndicatorId.worker]: this.workerHealth,
|
||||
[HealthIndicatorId.connectedAccount]: this.connectedAccountHealth,
|
||||
[HealthIndicatorId.app]: this.appHealth,
|
||||
};
|
||||
|
||||
private transformStatus(status: HealthIndicatorStatus) {
|
||||
@ -46,15 +49,17 @@ export class AdminPanelHealthService {
|
||||
private transformServiceDetails(details: any) {
|
||||
if (!details) return details;
|
||||
|
||||
if (details.messageSync) {
|
||||
details.messageSync.status = this.transformStatus(
|
||||
details.messageSync.status,
|
||||
);
|
||||
}
|
||||
if (details.calendarSync) {
|
||||
details.calendarSync.status = this.transformStatus(
|
||||
details.calendarSync.status,
|
||||
);
|
||||
if (details.details) {
|
||||
if (details.details.messageSync) {
|
||||
details.details.messageSync.status = this.transformStatus(
|
||||
details.details.messageSync.status,
|
||||
);
|
||||
}
|
||||
if (details.details.calendarSync) {
|
||||
details.details.calendarSync.status = this.transformStatus(
|
||||
details.details.calendarSync.status,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return details;
|
||||
@ -65,17 +70,33 @@ export class AdminPanelHealthService {
|
||||
indicatorId: HealthIndicatorId,
|
||||
) {
|
||||
if (result.status === 'fulfilled') {
|
||||
const key = Object.keys(result.value)[0];
|
||||
const keys = Object.keys(result.value);
|
||||
|
||||
if (keys.length === 0) {
|
||||
return {
|
||||
...HEALTH_INDICATORS[indicatorId],
|
||||
status: AdminPanelHealthServiceStatus.OUTAGE,
|
||||
errorMessage: 'No health check result available',
|
||||
};
|
||||
}
|
||||
const key = keys[0];
|
||||
const serviceResult = result.value[key];
|
||||
const details = this.transformServiceDetails(serviceResult.details);
|
||||
const { status, message, ...detailsWithoutStatus } = serviceResult;
|
||||
const indicator = HEALTH_INDICATORS[indicatorId];
|
||||
|
||||
const transformedDetails =
|
||||
this.transformServiceDetails(detailsWithoutStatus);
|
||||
|
||||
return {
|
||||
id: indicatorId,
|
||||
label: indicator.label,
|
||||
description: indicator.description,
|
||||
status: this.transformStatus(serviceResult.status),
|
||||
details: details ? JSON.stringify(details) : undefined,
|
||||
status: this.transformStatus(status),
|
||||
errorMessage: message,
|
||||
details:
|
||||
Object.keys(transformedDetails).length > 0
|
||||
? JSON.stringify(transformedDetails)
|
||||
: undefined,
|
||||
queues: serviceResult.queues,
|
||||
};
|
||||
}
|
||||
@ -83,7 +104,10 @@ export class AdminPanelHealthService {
|
||||
return {
|
||||
...HEALTH_INDICATORS[indicatorId],
|
||||
status: AdminPanelHealthServiceStatus.OUTAGE,
|
||||
details: result.reason?.message?.toString(),
|
||||
errorMessage: result.reason?.message?.toString(),
|
||||
details: result.reason?.details
|
||||
? JSON.stringify(result.reason.details)
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
@ -117,13 +141,19 @@ export class AdminPanelHealthService {
|
||||
}
|
||||
|
||||
async getSystemHealthStatus(): Promise<SystemHealth> {
|
||||
const [databaseResult, redisResult, workerResult, accountSyncResult] =
|
||||
await Promise.allSettled([
|
||||
this.databaseHealth.isHealthy(),
|
||||
this.redisHealth.isHealthy(),
|
||||
this.workerHealth.isHealthy(),
|
||||
this.connectedAccountHealth.isHealthy(),
|
||||
]);
|
||||
const [
|
||||
databaseResult,
|
||||
redisResult,
|
||||
workerResult,
|
||||
accountSyncResult,
|
||||
appResult,
|
||||
] = await Promise.allSettled([
|
||||
this.databaseHealth.isHealthy(),
|
||||
this.redisHealth.isHealthy(),
|
||||
this.workerHealth.isHealthy(),
|
||||
this.connectedAccountHealth.isHealthy(),
|
||||
this.appHealth.isHealthy(),
|
||||
]);
|
||||
|
||||
return {
|
||||
services: [
|
||||
@ -151,6 +181,11 @@ export class AdminPanelHealthService {
|
||||
HealthIndicatorId.connectedAccount,
|
||||
).status,
|
||||
},
|
||||
{
|
||||
...HEALTH_INDICATORS[HealthIndicatorId.app],
|
||||
status: this.getServiceStatus(appResult, HealthIndicatorId.app)
|
||||
.status,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
@ -28,4 +28,9 @@ export const HEALTH_INDICATORS: Record<HealthIndicatorId, HealthIndicatorInfo> =
|
||||
label: 'Connected Account Status',
|
||||
description: 'Connected accounts status',
|
||||
},
|
||||
[HealthIndicatorId.app]: {
|
||||
id: HealthIndicatorId.app,
|
||||
label: 'App Status',
|
||||
description: 'Workspace metadata migration status check',
|
||||
},
|
||||
};
|
||||
|
||||
@ -17,6 +17,8 @@ export class AdminPanelHealthServiceData {
|
||||
@Field(() => AdminPanelHealthServiceStatus)
|
||||
status: AdminPanelHealthServiceStatus;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
errorMessage?: string;
|
||||
@Field(() => String, { nullable: true })
|
||||
details?: string;
|
||||
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import { registerEnumType } from '@nestjs/graphql';
|
||||
|
||||
export enum AdminPanelHealthServiceStatus {
|
||||
OPERATIONAL = 'operational',
|
||||
OUTAGE = 'outage',
|
||||
OPERATIONAL = 'OPERATIONAL',
|
||||
OUTAGE = 'OUTAGE',
|
||||
}
|
||||
|
||||
registerEnumType(AdminPanelHealthServiceStatus, {
|
||||
|
||||
Reference in New Issue
Block a user