Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make some health-check information more clear #2

Merged
merged 5 commits into from
Oct 30, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/real-guests-smash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@dts-stn/health-checks": major
---

- UNKNOWN status changed to TIMEDOUT.
- Timing properties changed to responseTimeMs and timoutMs.
2 changes: 1 addition & 1 deletion packages/health-checks/other/health-checks.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"type": "object",
"definitions": {
"healthStatus": {
"enum": ["HEALTHY", "UNHEALTHY", "UNKNOWN"]
"enum": ["HEALTHY", "UNHEALTHY", "TIMEDOUT"]
}
},
"properties": {
Expand Down
42 changes: 21 additions & 21 deletions packages/health-checks/src/health-checks.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* Represents the health state of a component or system.
*/
export type HealthStatus = 'HEALTHY' | 'UNHEALTHY' | 'UNKNOWN';
export type HealthStatus = 'HEALTHY' | 'UNHEALTHY' | 'TIMEDOUT';

/**
* Represents a health check for a specific system component.
Expand All @@ -16,7 +16,7 @@ export interface HealthCheck {
* Configuration options for running health checks.
*/
export interface HealthCheckOptions {
timeout?: number;
timeoutMs?: number;
includeDetails?: boolean;
includeComponents?: string[];
excludeComponents?: string[];
Expand All @@ -33,7 +33,7 @@ export interface SystemHealthSummary {
readonly status: HealthStatus;
readonly buildId?: string;
readonly version?: string;
readonly responseTime: number;
readonly responseTimeMs: number;
readonly components?: Record<string, ComponentSummary>[];
}

Expand All @@ -47,7 +47,7 @@ export const HealthCheckConfig = {
},
responses: {
contentType: 'application/health+json',
statusCodes: { healthy: 200, unhealthy: 503, unknown: 500 },
statusCodes: { healthy: 200, unhealthy: 503, timedout: 408 },
},
} as const;

Expand All @@ -72,17 +72,17 @@ type HealthCheckResult = HealthCheckSuccess | HealthCheckFailure;
interface HealthCheckSuccess {
readonly status: 'HEALTHY';
readonly name: string;
readonly responseTime: number;
readonly responseTimeMs: number;
readonly metadata?: Record<string, string>;
}

/**
* Represents a failed component health check result.
*/
interface HealthCheckFailure {
readonly status: 'UNHEALTHY' | 'UNKNOWN';
readonly status: 'UNHEALTHY' | 'TIMEDOUT';
readonly name: string;
readonly responseTime: number;
readonly responseTimeMs: number;
readonly metadata?: Record<string, string>;
readonly errorDetails: string;
readonly stackTrace?: string;
Expand All @@ -98,16 +98,16 @@ type ComponentSummary = ComponentSuccessSummary | ComponentFailureSummary;
*/
interface ComponentSuccessSummary {
readonly status: 'HEALTHY';
readonly responseTime: number;
readonly responseTimeMs: number;
readonly metadata?: Record<string, string>;
}

/**
* Summarizes a failed health check for a component.
*/
interface ComponentFailureSummary {
readonly status: 'UNHEALTHY' | 'UNKNOWN';
readonly responseTime: number;
readonly status: 'UNHEALTHY' | 'TIMEDOUT';
readonly responseTimeMs: number;
readonly metadata?: Record<string, string>;
readonly errorDetails?: string;
readonly stackTrace?: string;
Expand All @@ -121,7 +121,7 @@ export async function execute(
options: HealthCheckOptions = {},
): Promise<SystemHealthSummary> {
const {
timeout = HealthCheckConfig.defaults.timeout,
timeoutMs = HealthCheckConfig.defaults.timeout,
includeDetails = HealthCheckConfig.defaults.includeDetails,
includeComponents = healthChecks.map((check) => check.name),
excludeComponents = [],
Expand All @@ -133,13 +133,13 @@ export async function execute(
);

const startTime = Date.now();
const results = await Promise.all(enabledChecks.map((check) => executeWithTimeout(check, timeout)));
const responseTime = Date.now() - startTime;
const results = await Promise.all(enabledChecks.map((check) => executeWithTimeout(check, timeoutMs)));
const responseTimeMs = Date.now() - startTime;

return {
buildId: metadata?.buildId,
components: results.map((result) => createComponentSummary(result, includeDetails)),
responseTime,
responseTimeMs,
status: results.map((result) => result.status).reduce(aggregateHealthStatus, 'HEALTHY'),
version: metadata?.version,
};
Expand Down Expand Up @@ -170,17 +170,17 @@ export async function executeWithTimeout(healthCheck: HealthCheck, timeout: numb
return {
metadata: healthCheck.metadata,
name: healthCheck.name,
responseTime: Date.now() - startTime,
responseTimeMs: Date.now() - startTime,
status: 'HEALTHY',
};
} catch (error) {
return {
errorDetails: error instanceof Error ? error.message : JSON.stringify(error),
metadata: healthCheck.metadata,
name: healthCheck.name,
responseTime: Date.now() - startTime,
responseTimeMs: Date.now() - startTime,
stackTrace: error instanceof Error ? error.stack : undefined,
status: error instanceof HealthCheckTimeoutError ? 'UNKNOWN' : 'UNHEALTHY',
status: error instanceof HealthCheckTimeoutError ? 'TIMEDOUT' : 'UNHEALTHY',
};
} finally {
// finished... abort all unresolved promises
Expand All @@ -196,9 +196,9 @@ export function aggregateHealthStatus(prevStatus: HealthStatus, currStatus: Heal
if (prevStatus === 'UNHEALTHY') return 'UNHEALTHY';
if (currStatus === 'UNHEALTHY') return 'UNHEALTHY';

// then prioritize UNKNOWN status
if (prevStatus === 'UNKNOWN') return 'UNKNOWN';
if (currStatus === 'UNKNOWN') return 'UNKNOWN';
// then prioritize TIMEDOUT status
if (prevStatus === 'TIMEDOUT') return 'UNHEALTHY';
if (currStatus === 'TIMEDOUT') return 'UNHEALTHY';

// guess we're HEALTHY 🏆
return 'HEALTHY';
Expand All @@ -216,7 +216,7 @@ export function createComponentSummary(

const result: ComponentSummary = {
status: healthCheckResult.status,
responseTime: healthCheckResult.responseTime,
responseTimeMs: healthCheckResult.responseTimeMs,
...(includeMetadata && {
metadata: healthCheckResult.metadata,
}),
Expand Down
50 changes: 25 additions & 25 deletions packages/health-checks/test/health-checks.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,13 @@ describe('aggregateHealthStatus(..)', () => {
const cases: [HealthStatus, HealthStatus, HealthStatus][] = [
['HEALTHY', 'HEALTHY', 'HEALTHY'],
['HEALTHY', 'UNHEALTHY', 'UNHEALTHY'],
['HEALTHY', 'UNKNOWN', 'UNKNOWN'],
['HEALTHY', 'TIMEDOUT', 'UNHEALTHY'],
['UNHEALTHY', 'HEALTHY', 'UNHEALTHY'],
['UNHEALTHY', 'UNHEALTHY', 'UNHEALTHY'],
['UNHEALTHY', 'UNKNOWN', 'UNHEALTHY'],
['UNKNOWN', 'HEALTHY', 'UNKNOWN'],
['UNKNOWN', 'UNHEALTHY', 'UNHEALTHY'],
['UNKNOWN', 'UNKNOWN', 'UNKNOWN'],
['UNHEALTHY', 'TIMEDOUT', 'UNHEALTHY'],
['TIMEDOUT', 'HEALTHY', 'UNHEALTHY'],
['TIMEDOUT', 'UNHEALTHY', 'UNHEALTHY'],
['TIMEDOUT', 'TIMEDOUT', 'UNHEALTHY'],
];

cases.forEach(([prevStatus, currStatus, expectedStatus]) => {
Expand Down Expand Up @@ -61,11 +61,11 @@ describe('run(..)', () => {
{ name: 'component2', check: () => delay(timeout << 2) },
];

const summary = execute(healthChecks, { timeout: timeout });
const summary = execute(healthChecks, { timeoutMs: timeout });
vi.advanceTimersByTime(timeout);
const { status } = await summary;

expect(status).toEqual('UNKNOWN');
expect(status).toEqual('UNHEALTHY');
});

it('should return correct component details', async () => {
Expand Down Expand Up @@ -101,14 +101,14 @@ describe('run(..)', () => {
{
component1: {
status: 'HEALTHY',
responseTime: 100,
responseTimeMs: 100,
metadata: { foo: 'bar' },
},
},
{
component2: {
status: 'UNHEALTHY',
responseTime: 0,
responseTimeMs: 0,
errorDetails: 'Something went wrong',
stackTrace: expect.any(String) as string,
},
Expand Down Expand Up @@ -146,7 +146,7 @@ describe('run(..)', () => {
vi.advanceTimersByTime(HealthCheckConfig.defaults.timeout);
const { status } = await summary;

expect(status).toEqual('UNKNOWN');
expect(status).toEqual('UNHEALTHY');
});

it('should filter components based on includeComponents', async () => {
Expand Down Expand Up @@ -188,21 +188,21 @@ describe('runWithTimeout(..)', () => {

const result = executeWithTimeout(healthCheck, 100);
vi.advanceTimersByTime(50);
const { responseTime, status } = await result;
const { responseTimeMs, status } = await result;

expect(status).toEqual('HEALTHY');
expect(responseTime).toEqual(50);
expect(responseTimeMs).toEqual(50);
});

it('should resolve with UNKNOWN status for timed out check', async () => {
it('should resolve with TIMEDOUT status for timed out check', async () => {
const healthCheck: HealthCheck = { name: 'testComponent', check: () => delay(200) };

const result = executeWithTimeout(healthCheck, 100);
vi.advanceTimersByTime(100);
const { responseTime, status } = await result;
const { responseTimeMs, status } = await result;

expect(status).toEqual('UNKNOWN');
expect(responseTime).toEqual(100);
expect(status).toEqual('TIMEDOUT');
expect(responseTimeMs).toEqual(100);
});

it('should resolve with UNHEALTHY status for failed check', async () => {
Expand All @@ -215,10 +215,10 @@ describe('runWithTimeout(..)', () => {

const result = executeWithTimeout(healthCheck, 100);
vi.advanceTimersByTime(100);
const { responseTime, status } = await result;
const { responseTimeMs, status } = await result;

expect(status).toEqual('UNHEALTHY');
expect(responseTime).toEqual(0);
expect(responseTimeMs).toEqual(0);
});

it('should include component metadata in the result', async () => {
Expand All @@ -238,7 +238,7 @@ describe('createComponentSummary(..)', () => {
{
status: 'HEALTHY',
name: 'testComponent',
responseTime: 50,
responseTimeMs: 50,
metadata: { foo: 'bar' },
},
true,
Expand All @@ -247,7 +247,7 @@ describe('createComponentSummary(..)', () => {
expect(summary).toEqual({
testComponent: {
status: 'HEALTHY',
responseTime: 50,
responseTimeMs: 50,
metadata: { foo: 'bar' },
},
});
Expand All @@ -258,7 +258,7 @@ describe('createComponentSummary(..)', () => {
{
status: 'UNHEALTHY',
name: 'testComponent',
responseTime: 50,
responseTimeMs: 50,
errorDetails: 'Something went wrong',
metadata: { foo: 'bar' },
},
Expand All @@ -268,7 +268,7 @@ describe('createComponentSummary(..)', () => {
expect(summary).toEqual({
testComponent: {
status: 'UNHEALTHY',
responseTime: 50,
responseTimeMs: 50,
errorDetails: 'Something went wrong',
metadata: { foo: 'bar' },
},
Expand All @@ -280,7 +280,7 @@ describe('createComponentSummary(..)', () => {
{
status: 'UNHEALTHY',
name: 'testComponent',
responseTime: 50,
responseTimeMs: 50,
errorDetails: 'Something went wrong',
metadata: { foo: 'bar' },
},
Expand All @@ -290,7 +290,7 @@ describe('createComponentSummary(..)', () => {
expect(summary).toEqual({
testComponent: {
status: 'UNHEALTHY',
responseTime: 50,
responseTimeMs: 50,
},
});
});
Expand All @@ -300,7 +300,7 @@ describe('getStatusCode(..)', () => {
const cases: [HealthStatus, number][] = [
['HEALTHY', 200],
['UNHEALTHY', 503],
['UNKNOWN', 500],
['TIMEDOUT', 408],
];

cases.forEach(([healthStatus, expectedHttpStatus]) => {
Expand Down
Loading