refactor + new account sync metrics + isolating health status inside folder admin-panel > health-status (#10314)

closes https://github.com/twentyhq/core-team-issues/issues/444
https://github.com/twentyhq/core-team-issues/issues/443
https://github.com/twentyhq/core-team-issues/issues/442
This commit is contained in:
nitin
2025-02-21 14:18:47 +05:30
committed by GitHub
parent 41bbb4b47f
commit c46f7848b7
57 changed files with 1441 additions and 833 deletions

View File

@ -1,11 +1,13 @@
import { Test, TestingModule } from '@nestjs/testing';
import { AdminPanelHealthService } from 'src/engine/core-modules/admin-panel/admin-panel-health.service';
import { HEALTH_INDICATORS } from 'src/engine/core-modules/admin-panel/constants/health-indicators.constants';
import { SystemHealth } from 'src/engine/core-modules/admin-panel/dtos/system-health.dto';
import { AdminPanelHealthServiceStatus } from 'src/engine/core-modules/admin-panel/enums/admin-panel-health-service-status.enum';
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 { ConnectedAccountHealth } from 'src/engine/core-modules/health/indicators/connected-account.health';
import { DatabaseHealthIndicator } from 'src/engine/core-modules/health/indicators/database.health';
import { MessageSyncHealthIndicator } from 'src/engine/core-modules/health/indicators/message-sync.health';
import { RedisHealthIndicator } from 'src/engine/core-modules/health/indicators/redis.health';
import { WorkerHealthIndicator } from 'src/engine/core-modules/health/indicators/worker.health';
@ -14,44 +16,21 @@ describe('AdminPanelHealthService', () => {
let databaseHealth: jest.Mocked<DatabaseHealthIndicator>;
let redisHealth: jest.Mocked<RedisHealthIndicator>;
let workerHealth: jest.Mocked<WorkerHealthIndicator>;
let messageSyncHealth: jest.Mocked<MessageSyncHealthIndicator>;
let connectedAccountHealth: jest.Mocked<ConnectedAccountHealth>;
beforeEach(async () => {
databaseHealth = {
isHealthy: jest.fn(),
} as any;
redisHealth = {
isHealthy: jest.fn(),
} as any;
workerHealth = {
isHealthy: jest.fn(),
} as any;
messageSyncHealth = {
isHealthy: jest.fn(),
} as any;
databaseHealth = { isHealthy: jest.fn() } as any;
redisHealth = { isHealthy: jest.fn() } as any;
workerHealth = { isHealthy: jest.fn() } as any;
connectedAccountHealth = { isHealthy: jest.fn() } as any;
const module: TestingModule = await Test.createTestingModule({
providers: [
AdminPanelHealthService,
{
provide: DatabaseHealthIndicator,
useValue: databaseHealth,
},
{
provide: RedisHealthIndicator,
useValue: redisHealth,
},
{
provide: WorkerHealthIndicator,
useValue: workerHealth,
},
{
provide: MessageSyncHealthIndicator,
useValue: messageSyncHealth,
},
{ provide: DatabaseHealthIndicator, useValue: databaseHealth },
{ provide: RedisHealthIndicator, useValue: redisHealth },
{ provide: WorkerHealthIndicator, useValue: workerHealth },
{ provide: ConnectedAccountHealth, useValue: connectedAccountHealth },
],
}).compile();
@ -62,132 +41,244 @@ describe('AdminPanelHealthService', () => {
expect(service).toBeDefined();
});
it('should transform health check response to SystemHealth format', async () => {
databaseHealth.isHealthy.mockResolvedValue({
database: {
status: 'up',
details: 'Database is healthy',
},
});
redisHealth.isHealthy.mockResolvedValue({
redis: {
status: 'up',
details: 'Redis is connected',
},
});
workerHealth.isHealthy.mockResolvedValue({
worker: {
status: 'up',
queues: [
{
name: 'test',
workers: 1,
metrics: {
active: 1,
completed: 0,
delayed: 4,
failed: 3,
waiting: 0,
prioritized: 0,
describe('getSystemHealthStatus', () => {
it('should transform health check response to SystemHealth format', async () => {
databaseHealth.isHealthy.mockResolvedValue({
database: { status: 'up', details: 'Database is healthy' },
});
redisHealth.isHealthy.mockResolvedValue({
redis: { status: 'up', details: 'Redis is connected' },
});
workerHealth.isHealthy.mockResolvedValue({
worker: {
status: 'up',
queues: [
{
queueName: 'test',
workers: 1,
metrics: {
active: 1,
completed: 0,
delayed: 4,
failed: 3,
waiting: 0,
prioritized: 0,
},
},
],
},
});
connectedAccountHealth.isHealthy.mockResolvedValue({
connectedAccount: {
status: 'up',
details: 'Account sync is operational',
},
});
const result = await service.getSystemHealthStatus();
const expected: SystemHealth = {
services: [
{
...HEALTH_INDICATORS[HealthIndicatorId.database],
status: AdminPanelHealthServiceStatus.OPERATIONAL,
},
{
...HEALTH_INDICATORS[HealthIndicatorId.redis],
status: AdminPanelHealthServiceStatus.OPERATIONAL,
},
{
...HEALTH_INDICATORS[HealthIndicatorId.worker],
status: AdminPanelHealthServiceStatus.OPERATIONAL,
},
{
...HEALTH_INDICATORS[HealthIndicatorId.connectedAccount],
status: AdminPanelHealthServiceStatus.OPERATIONAL,
},
],
},
});
messageSyncHealth.isHealthy.mockResolvedValue({
messageSync: {
status: 'up',
details: 'Message sync is operational',
},
};
expect(result).toStrictEqual(expected);
});
const result = await service.getSystemHealthStatus();
it('should handle mixed health statuses', async () => {
databaseHealth.isHealthy.mockResolvedValue({
database: { status: 'up' },
});
redisHealth.isHealthy.mockRejectedValue(
new Error(HEALTH_ERROR_MESSAGES.REDIS_CONNECTION_FAILED),
);
workerHealth.isHealthy.mockResolvedValue({
worker: { status: 'up', queues: [] },
});
connectedAccountHealth.isHealthy.mockResolvedValue({
connectedAccount: { status: 'up' },
});
const expected: SystemHealth = {
database: {
const result = await service.getSystemHealthStatus();
expect(result).toStrictEqual({
services: [
{
...HEALTH_INDICATORS[HealthIndicatorId.database],
status: AdminPanelHealthServiceStatus.OPERATIONAL,
},
{
...HEALTH_INDICATORS[HealthIndicatorId.redis],
status: AdminPanelHealthServiceStatus.OUTAGE,
},
{
...HEALTH_INDICATORS[HealthIndicatorId.worker],
status: AdminPanelHealthServiceStatus.OPERATIONAL,
},
{
...HEALTH_INDICATORS[HealthIndicatorId.connectedAccount],
status: AdminPanelHealthServiceStatus.OPERATIONAL,
},
],
});
});
it('should handle all services down', async () => {
databaseHealth.isHealthy.mockRejectedValue(
new Error(HEALTH_ERROR_MESSAGES.DATABASE_CONNECTION_FAILED),
);
redisHealth.isHealthy.mockRejectedValue(
new Error(HEALTH_ERROR_MESSAGES.REDIS_CONNECTION_FAILED),
);
workerHealth.isHealthy.mockRejectedValue(
new Error(HEALTH_ERROR_MESSAGES.NO_ACTIVE_WORKERS),
);
connectedAccountHealth.isHealthy.mockRejectedValue(
new Error(HEALTH_ERROR_MESSAGES.MESSAGE_SYNC_CHECK_FAILED),
);
const result = await service.getSystemHealthStatus();
expect(result).toStrictEqual({
services: [
{
...HEALTH_INDICATORS[HealthIndicatorId.database],
status: AdminPanelHealthServiceStatus.OUTAGE,
},
{
...HEALTH_INDICATORS[HealthIndicatorId.redis],
status: AdminPanelHealthServiceStatus.OUTAGE,
},
{
...HEALTH_INDICATORS[HealthIndicatorId.worker],
status: AdminPanelHealthServiceStatus.OUTAGE,
},
{
...HEALTH_INDICATORS[HealthIndicatorId.connectedAccount],
status: AdminPanelHealthServiceStatus.OUTAGE,
},
],
});
});
});
describe('getIndicatorHealthStatus', () => {
it('should return health status for database indicator', async () => {
const details = {
version: '15.0',
connections: { active: 5, max: 100 },
};
databaseHealth.isHealthy.mockResolvedValue({
database: {
status: 'up',
details,
},
});
const result = await service.getIndicatorHealthStatus(
HealthIndicatorId.database,
);
expect(result).toStrictEqual({
...HEALTH_INDICATORS[HealthIndicatorId.database],
status: AdminPanelHealthServiceStatus.OPERATIONAL,
details: '"Database is healthy"',
details: JSON.stringify(details),
queues: undefined,
},
redis: {
status: AdminPanelHealthServiceStatus.OPERATIONAL,
details: '"Redis is connected"',
queues: undefined,
},
worker: {
});
});
it('should return health status with queues for worker indicator', async () => {
const mockQueues = [
{
queueName: 'queue1',
workers: 2,
metrics: {
active: 1,
completed: 10,
delayed: 0,
failed: 2,
waiting: 5,
prioritized: 1,
},
},
{
queueName: 'queue2',
workers: 0,
metrics: {
active: 0,
completed: 5,
delayed: 0,
failed: 1,
waiting: 2,
prioritized: 0,
},
},
];
workerHealth.isHealthy.mockResolvedValue({
worker: {
status: 'up',
queues: mockQueues,
},
});
const result = await service.getIndicatorHealthStatus(
HealthIndicatorId.worker,
);
expect(result).toStrictEqual({
...HEALTH_INDICATORS[HealthIndicatorId.worker],
status: AdminPanelHealthServiceStatus.OPERATIONAL,
details: undefined,
queues: [
{
name: 'test',
workers: 1,
status: AdminPanelHealthServiceStatus.OPERATIONAL,
metrics: {
active: 1,
completed: 0,
delayed: 4,
failed: 3,
waiting: 0,
prioritized: 0,
},
},
],
},
messageSync: {
status: AdminPanelHealthServiceStatus.OPERATIONAL,
details: '"Message sync is operational"',
queues: undefined,
},
};
expect(result).toStrictEqual(expected);
});
it('should handle mixed health statuses', async () => {
databaseHealth.isHealthy.mockResolvedValue({
database: { status: 'up' },
});
redisHealth.isHealthy.mockRejectedValue(
new Error(HEALTH_ERROR_MESSAGES.REDIS_CONNECTION_FAILED),
);
workerHealth.isHealthy.mockResolvedValue({
worker: { status: 'up', queues: [] },
});
messageSyncHealth.isHealthy.mockResolvedValue({
messageSync: { status: 'up' },
queues: mockQueues.map((queue) => ({
...queue,
id: `worker-${queue.queueName}`,
status:
queue.workers > 0
? AdminPanelHealthServiceStatus.OPERATIONAL
: AdminPanelHealthServiceStatus.OUTAGE,
})),
});
});
const result = await service.getSystemHealthStatus();
it('should handle failed indicator health check', async () => {
redisHealth.isHealthy.mockRejectedValue(
new Error(HEALTH_ERROR_MESSAGES.REDIS_CONNECTION_FAILED),
);
expect(result).toMatchObject({
database: { status: AdminPanelHealthServiceStatus.OPERATIONAL },
redis: { status: AdminPanelHealthServiceStatus.OUTAGE },
worker: { status: AdminPanelHealthServiceStatus.OPERATIONAL },
messageSync: { status: AdminPanelHealthServiceStatus.OPERATIONAL },
const result = await service.getIndicatorHealthStatus(
HealthIndicatorId.redis,
);
expect(result).toStrictEqual({
...HEALTH_INDICATORS[HealthIndicatorId.redis],
status: AdminPanelHealthServiceStatus.OUTAGE,
details: HEALTH_ERROR_MESSAGES.REDIS_CONNECTION_FAILED,
});
});
});
it('should handle all services down', async () => {
databaseHealth.isHealthy.mockRejectedValue(
new Error(HEALTH_ERROR_MESSAGES.DATABASE_CONNECTION_FAILED),
);
redisHealth.isHealthy.mockRejectedValue(
new Error(HEALTH_ERROR_MESSAGES.REDIS_CONNECTION_FAILED),
);
workerHealth.isHealthy.mockRejectedValue(
new Error(HEALTH_ERROR_MESSAGES.NO_ACTIVE_WORKERS),
);
messageSyncHealth.isHealthy.mockRejectedValue(
new Error(HEALTH_ERROR_MESSAGES.MESSAGE_SYNC_CHECK_FAILED),
);
const result = await service.getSystemHealthStatus();
expect(result).toMatchObject({
database: { status: AdminPanelHealthServiceStatus.OUTAGE },
redis: { status: AdminPanelHealthServiceStatus.OUTAGE },
worker: { status: AdminPanelHealthServiceStatus.OUTAGE },
messageSync: { status: AdminPanelHealthServiceStatus.OUTAGE },
it('should throw error for invalid indicator', async () => {
await expect(
// @ts-expect-error Testing invalid input
service.getIndicatorHealthStatus('invalid'),
).rejects.toThrow('Health indicator not found: invalid');
});
});
});

View File

@ -1,12 +1,13 @@
import { Injectable } from '@nestjs/common';
import { HealthIndicatorResult } from '@nestjs/terminus';
import { HealthIndicatorResult, HealthIndicatorStatus } from '@nestjs/terminus';
import { HEALTH_INDICATORS } from 'src/engine/core-modules/admin-panel/constants/health-indicators.constants';
import { AdminPanelHealthServiceData } from 'src/engine/core-modules/admin-panel/dtos/admin-panel-health-service-data.dto';
import { AdminPanelIndicatorHealthStatusInputEnum } from 'src/engine/core-modules/admin-panel/dtos/admin-panel-indicator-health-status.input';
import { SystemHealth } from 'src/engine/core-modules/admin-panel/dtos/system-health.dto';
import { AdminPanelHealthServiceStatus } from 'src/engine/core-modules/admin-panel/enums/admin-panel-health-service-status.enum';
import { HealthIndicatorId } from 'src/engine/core-modules/health/enums/health-indicator-id.enum';
import { ConnectedAccountHealth } from 'src/engine/core-modules/health/indicators/connected-account.health';
import { DatabaseHealthIndicator } from 'src/engine/core-modules/health/indicators/database.health';
import { MessageSyncHealthIndicator } from 'src/engine/core-modules/health/indicators/message-sync.health';
import { RedisHealthIndicator } from 'src/engine/core-modules/health/indicators/redis.health';
import { WorkerHealthIndicator } from 'src/engine/core-modules/health/indicators/worker.health';
@ -16,57 +17,84 @@ export class AdminPanelHealthService {
private readonly databaseHealth: DatabaseHealthIndicator,
private readonly redisHealth: RedisHealthIndicator,
private readonly workerHealth: WorkerHealthIndicator,
private readonly messageSyncHealth: MessageSyncHealthIndicator,
private readonly connectedAccountHealth: ConnectedAccountHealth,
) {}
private readonly healthIndicators = {
database: this.databaseHealth,
redis: this.redisHealth,
worker: this.workerHealth,
messageSync: this.messageSyncHealth,
[HealthIndicatorId.database]: this.databaseHealth,
[HealthIndicatorId.redis]: this.redisHealth,
[HealthIndicatorId.worker]: this.workerHealth,
[HealthIndicatorId.connectedAccount]: this.connectedAccountHealth,
};
private transformStatus(status: HealthIndicatorStatus) {
return status === 'up'
? AdminPanelHealthServiceStatus.OPERATIONAL
: AdminPanelHealthServiceStatus.OUTAGE;
}
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,
);
}
return details;
}
private getServiceStatus(
result: PromiseSettledResult<HealthIndicatorResult>,
indicatorId: HealthIndicatorId,
) {
if (result.status === 'fulfilled') {
const key = Object.keys(result.value)[0];
const serviceResult = result.value[key];
const details = serviceResult.details;
const details = this.transformServiceDetails(serviceResult.details);
const indicator = HEALTH_INDICATORS[indicatorId];
return {
status:
serviceResult.status === 'up'
? AdminPanelHealthServiceStatus.OPERATIONAL
: AdminPanelHealthServiceStatus.OUTAGE,
id: indicatorId,
label: indicator.label,
description: indicator.description,
status: this.transformStatus(serviceResult.status),
details: details ? JSON.stringify(details) : undefined,
queues: serviceResult.queues,
};
}
return {
...HEALTH_INDICATORS[indicatorId],
status: AdminPanelHealthServiceStatus.OUTAGE,
details: result.reason?.message,
details: result.reason?.message?.toString(),
};
}
async getIndicatorHealthStatus(
indicatorName: AdminPanelIndicatorHealthStatusInputEnum,
indicatorId: HealthIndicatorId,
): Promise<AdminPanelHealthServiceData> {
const healthIndicator = this.healthIndicators[indicatorName];
const healthIndicator = this.healthIndicators[indicatorId];
if (!healthIndicator) {
throw new Error(`Health indicator not found: ${indicatorName}`);
throw new Error(`Health indicator not found: ${indicatorId}`);
}
const result = await Promise.allSettled([healthIndicator.isHealthy()]);
const indicatorStatus = this.getServiceStatus(result[0]);
const indicatorStatus = this.getServiceStatus(result[0], indicatorId);
if (indicatorName === 'worker') {
if (indicatorId === HealthIndicatorId.worker) {
return {
...indicatorStatus,
queues: (indicatorStatus?.queues ?? []).map((queue) => ({
...queue,
id: `${indicatorId}-${queue.queueName}`,
status:
queue.workers > 0
? AdminPanelHealthServiceStatus.OPERATIONAL
@ -79,30 +107,41 @@ export class AdminPanelHealthService {
}
async getSystemHealthStatus(): Promise<SystemHealth> {
const [databaseResult, redisResult, workerResult, messageSyncResult] =
const [databaseResult, redisResult, workerResult, accountSyncResult] =
await Promise.allSettled([
this.databaseHealth.isHealthy(),
this.redisHealth.isHealthy(),
this.workerHealth.isHealthy(),
this.messageSyncHealth.isHealthy(),
this.connectedAccountHealth.isHealthy(),
]);
const workerStatus = this.getServiceStatus(workerResult);
return {
database: this.getServiceStatus(databaseResult),
redis: this.getServiceStatus(redisResult),
worker: {
...workerStatus,
queues: (workerStatus?.queues ?? []).map((queue) => ({
...queue,
status:
queue.workers > 0
? AdminPanelHealthServiceStatus.OPERATIONAL
: AdminPanelHealthServiceStatus.OUTAGE,
})),
},
messageSync: this.getServiceStatus(messageSyncResult),
services: [
{
...HEALTH_INDICATORS[HealthIndicatorId.database],
status: this.getServiceStatus(
databaseResult,
HealthIndicatorId.database,
).status,
},
{
...HEALTH_INDICATORS[HealthIndicatorId.redis],
status: this.getServiceStatus(redisResult, HealthIndicatorId.redis)
.status,
},
{
...HEALTH_INDICATORS[HealthIndicatorId.worker],
status: this.getServiceStatus(workerResult, HealthIndicatorId.worker)
.status,
},
{
...HEALTH_INDICATORS[HealthIndicatorId.connectedAccount],
status: this.getServiceStatus(
accountSyncResult,
HealthIndicatorId.connectedAccount,
).status,
},
],
};
}
}

View File

@ -11,12 +11,12 @@ import { UpdateWorkspaceFeatureFlagInput } from 'src/engine/core-modules/admin-p
import { UserLookup } from 'src/engine/core-modules/admin-panel/dtos/user-lookup.entity';
import { UserLookupInput } from 'src/engine/core-modules/admin-panel/dtos/user-lookup.input';
import { AuthGraphqlApiExceptionFilter } from 'src/engine/core-modules/auth/filters/auth-graphql-api-exception.filter';
import { HealthIndicatorId } from 'src/engine/core-modules/health/enums/health-indicator-id.enum';
import { ImpersonateGuard } from 'src/engine/guards/impersonate-guard';
import { UserAuthGuard } from 'src/engine/guards/user-auth.guard';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
import { AdminPanelHealthServiceData } from './dtos/admin-panel-health-service-data.dto';
import { AdminPanelIndicatorHealthStatusInputEnum } from './dtos/admin-panel-indicator-health-status.input';
@Resolver()
@UseFilters(AuthGraphqlApiExceptionFilter)
@ -70,11 +70,11 @@ export class AdminPanelResolver {
@Query(() => AdminPanelHealthServiceData)
async getIndicatorHealthStatus(
@Args('indicatorName', {
type: () => AdminPanelIndicatorHealthStatusInputEnum,
@Args('indicatorId', {
type: () => HealthIndicatorId,
})
indicatorName: AdminPanelIndicatorHealthStatusInputEnum,
indicatorId: HealthIndicatorId,
): Promise<AdminPanelHealthServiceData> {
return this.adminPanelHealthService.getIndicatorHealthStatus(indicatorName);
return this.adminPanelHealthService.getIndicatorHealthStatus(indicatorId);
}
}

View File

@ -0,0 +1,31 @@
import { HealthIndicatorId } from 'src/engine/core-modules/health/enums/health-indicator-id.enum';
type HealthIndicatorInfo = {
id: HealthIndicatorId;
label: string;
description: string;
};
export const HEALTH_INDICATORS: Record<HealthIndicatorId, HealthIndicatorInfo> =
{
[HealthIndicatorId.database]: {
id: HealthIndicatorId.database,
label: 'Database Status',
description: 'PostgreSQL database connection status',
},
[HealthIndicatorId.redis]: {
id: HealthIndicatorId.redis,
label: 'Redis Status',
description: 'Redis connection status',
},
[HealthIndicatorId.worker]: {
id: HealthIndicatorId.worker,
label: 'Worker Status',
description: 'Background job worker status',
},
[HealthIndicatorId.connectedAccount]: {
id: HealthIndicatorId.connectedAccount,
label: 'Connected Account Status',
description: 'Connected accounts status',
},
};

View File

@ -0,0 +1,8 @@
import { Field } from '@nestjs/graphql';
import { HealthIndicatorId } from 'src/engine/core-modules/health/enums/health-indicator-id.enum';
export class HealthIndicatorInput {
@Field(() => HealthIndicatorId)
indicatorId: HealthIndicatorId;
}

View File

@ -5,6 +5,15 @@ import { AdminPanelHealthServiceStatus } from 'src/engine/core-modules/admin-pan
@ObjectType()
export class AdminPanelHealthServiceData {
@Field(() => String)
id: string;
@Field(() => String)
label: string;
@Field(() => String)
description: string;
@Field(() => AdminPanelHealthServiceStatus)
status: AdminPanelHealthServiceStatus;

View File

@ -1,17 +0,0 @@
import { Field, registerEnumType } from '@nestjs/graphql';
export enum AdminPanelIndicatorHealthStatusInputEnum {
DATABASE = 'database',
REDIS = 'redis',
WORKER = 'worker',
MESSAGE_SYNC = 'messageSync',
}
registerEnumType(AdminPanelIndicatorHealthStatusInputEnum, {
name: 'AdminPanelIndicatorHealthStatusInputEnum',
});
export class AdminPanelIndicatorHealthStatusInput {
@Field(() => AdminPanelIndicatorHealthStatusInputEnum)
indicatorName: AdminPanelIndicatorHealthStatusInputEnum;
}

View File

@ -5,6 +5,9 @@ import { WorkerQueueHealth } from 'src/engine/core-modules/health/types/worker-q
@ObjectType()
export class AdminPanelWorkerQueueHealth extends WorkerQueueHealth {
@Field(() => String)
id: string;
@Field(() => AdminPanelHealthServiceStatus)
status: AdminPanelHealthServiceStatus;
}

View File

@ -1,18 +1,22 @@
import { Field, ObjectType } from '@nestjs/graphql';
import { AdminPanelHealthServiceData } from 'src/engine/core-modules/admin-panel/dtos/admin-panel-health-service-data.dto';
import { AdminPanelHealthServiceStatus } from 'src/engine/core-modules/admin-panel/enums/admin-panel-health-service-status.enum';
import { HealthIndicatorId } from 'src/engine/core-modules/health/enums/health-indicator-id.enum';
@ObjectType()
export class SystemHealthService {
@Field(() => HealthIndicatorId)
id: HealthIndicatorId;
@Field(() => String)
label: string;
@Field(() => AdminPanelHealthServiceStatus)
status: AdminPanelHealthServiceStatus;
}
@ObjectType()
export class SystemHealth {
@Field(() => AdminPanelHealthServiceData)
database: AdminPanelHealthServiceData;
@Field(() => AdminPanelHealthServiceData)
redis: AdminPanelHealthServiceData;
@Field(() => AdminPanelHealthServiceData)
worker: AdminPanelHealthServiceData;
@Field(() => AdminPanelHealthServiceData)
messageSync: AdminPanelHealthServiceData;
@Field(() => [SystemHealthService])
services: SystemHealthService[];
}