Skip to content

Commit

Permalink
Merge pull request #2510 from nestjs/refactor/health-indicator-service
Browse files Browse the repository at this point in the history
Create HealthIndicatorService to simplify custom health indicators
  • Loading branch information
BrunnerLivio authored Jan 23, 2025
2 parents 0ef44fa + 9f10a9b commit 4a830f1
Show file tree
Hide file tree
Showing 36 changed files with 652 additions and 461 deletions.
1 change: 1 addition & 0 deletions .github/workflows/build-samples.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,5 +39,6 @@ jobs:
# FIXME: Remove the `--legacy-peer-deps` flag once all dependencies are updated
- run: npm ci --legacy-peer-deps
- run: npm run build:all
- run: npm run test:samples
env:
CI: true
6 changes: 6 additions & 0 deletions lib/errors/connection-not-found.error.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
/* eslint-disable deprecation/deprecation */
import { CONNECTION_NOT_FOUND } from './messages.constant';
import { HealthCheckError } from '../health-check/health-check.error';

/**
* Error which gets thrown when the connection
* instance was not found in the application context
* @publicApi
*
* @deprecated
* This class has been deprecated and will be removed in the next major release.
* Instead utilise the `HealthIndicatorService` to indicate the health of your health indicator.
*
*/
export class ConnectionNotFoundError extends HealthCheckError {
/**
Expand Down
5 changes: 5 additions & 0 deletions lib/errors/storage-exceeded.error.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
/* eslint-disable deprecation/deprecation */
import { STORAGE_EXCEEDED } from './messages.constant';
import { HealthCheckError } from '../health-check/health-check.error';

/**
* Error which gets thrown when the given storage threshold
* has exceeded.
* @publicApi
*
* @deprecated
* This class has been deprecated and will be removed in the next major release.
* Instead utilise the `HealthIndicatorService` to indicate the health of your health indicator.
*/
export class StorageExceededError extends HealthCheckError {
/**
Expand Down
5 changes: 5 additions & 0 deletions lib/errors/timeout-error.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
/* eslint-disable deprecation/deprecation */
import { TIMEOUT_EXCEEDED } from './messages.constant';
import { HealthCheckError } from '../health-check/health-check.error';

/**
* Gets thrown when the timeout of the health check exceeds
* @publicApi
*
* @deprecated
* This class has been deprecated and will be removed in the next major release.
* Instead utilise the `HealthIndicatorService` to indicate the health of your health indicator.
*/
export class TimeoutError extends HealthCheckError {
/**
Expand Down
5 changes: 5 additions & 0 deletions lib/errors/unhealthy-response-code.error.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
/* eslint-disable deprecation/deprecation */
import { UNHEALTHY_RESPONSE_CODE } from './messages.constant';
import { HealthCheckError } from '../health-check/health-check.error';

/**
* Error which gets thrown when the terminus client receives
* an unhealthy response code from the server.
* @publicApi
*
* @deprecated
* This class has been deprecated and will be removed in the next major release.
* Instead utilise the `HealthIndicatorService` to indicate the health of your health indicator.
*/
export class UnhealthyResponseCodeError extends HealthCheckError {
/**
Expand Down
161 changes: 151 additions & 10 deletions lib/health-check/health-check-executor.service.spec.ts
Original file line number Diff line number Diff line change
@@ -1,47 +1,78 @@
import { Test } from '@nestjs/testing';
import { HealthCheckExecutor } from './health-check-executor.service';
import { HealthIndicatorResult } from '../health-indicator';
import {
HealthIndicatorResult,
HealthIndicatorService,
} from '../health-indicator';
import { HealthCheckResult } from './health-check-result.interface';
import { HealthCheckError } from '../health-check/health-check.error';

const healthyCheck = async (): Promise<HealthIndicatorResult> => {
////////////////////////////////////////////////////////////////

const healthIndicator = async (h: HealthIndicatorService) =>
h.check('healthy').up();

const unhealthyHealthIndicator = async (h: HealthIndicatorService) =>
h.check('unhealthy').down();

const unhealthyHealthIndicatorSync = (h: HealthIndicatorService) =>
h.check('unhealthy').down();

// Legacy health indicator functions

const legacyHealthyIndicator = async (): Promise<HealthIndicatorResult> => {
return {
healthy: {
status: 'up',
},
};
};

const unhealthyCheck = async (): Promise<HealthIndicatorResult> => {
const legacyUnhealthyIndicator = async (): Promise<HealthIndicatorResult> => {
throw new HealthCheckError('error', {
unhealthy: {
status: 'down',
},
});
};

const unhealthyCheckSync = () => {
const legacyUnhealthyIndicatorSync = () => {
throw new HealthCheckError('error', {
unhealthy: {
status: 'down',
},
});
};

const legacyUnhealthyIndicatorWithoutError =
async (): Promise<HealthIndicatorResult> => {
return {
unhealthy: {
status: 'down',
},
};
};

////////////////////////////////////////////////////////////////

describe('HealthCheckExecutorService', () => {
let healthCheckExecutor: HealthCheckExecutor;
let h: HealthIndicatorService;

beforeEach(async () => {
const module = Test.createTestingModule({
providers: [HealthCheckExecutor],
providers: [HealthCheckExecutor, HealthIndicatorService],
});
const context = await module.compile();
healthCheckExecutor = context.get(HealthCheckExecutor);
h = context.get(HealthIndicatorService);
});

describe('execute', () => {
it('should return a result object without errors', async () => {
const result = await healthCheckExecutor.execute([() => healthyCheck()]);
const result = await healthCheckExecutor.execute([
() => healthIndicator(h),
]);
expect(result).toEqual<HealthCheckResult>({
status: 'ok',
info: {
Expand All @@ -60,7 +91,7 @@ describe('HealthCheckExecutorService', () => {

it('should return a result object with errors', async () => {
const result = await healthCheckExecutor.execute([
() => unhealthyCheck(),
() => unhealthyHealthIndicator(h),
]);
expect(result).toEqual<HealthCheckResult>({
status: 'error',
Expand All @@ -80,7 +111,7 @@ describe('HealthCheckExecutorService', () => {

it('should return a result object with errors with sync indicator function', async () => {
const result = await healthCheckExecutor.execute([
() => unhealthyCheckSync(),
() => unhealthyHealthIndicatorSync(h),
]);
expect(result).toEqual<HealthCheckResult>({
status: 'error',
Expand All @@ -100,8 +131,8 @@ describe('HealthCheckExecutorService', () => {

it('should return a result object without errors and with errors', async () => {
const result = await healthCheckExecutor.execute([
() => unhealthyCheck(),
() => healthyCheck(),
() => unhealthyHealthIndicator(h),
() => healthIndicator(h),
]);
expect(result).toEqual<HealthCheckResult>({
status: 'error',
Expand All @@ -125,5 +156,115 @@ describe('HealthCheckExecutorService', () => {
},
});
});

describe('backwards compatibility', () => {
it('should return a result object without errors', async () => {
const result = await healthCheckExecutor.execute([
() => legacyHealthyIndicator(),
]);
expect(result).toEqual<HealthCheckResult>({
status: 'ok',
info: {
healthy: {
status: 'up',
},
},
error: {},
details: {
healthy: {
status: 'up',
},
},
});
});

it('should return a result object with errors', async () => {
const result = await healthCheckExecutor.execute([
() => legacyUnhealthyIndicator(),
]);
expect(result).toEqual<HealthCheckResult>({
status: 'error',
info: {},
error: {
unhealthy: {
status: 'down',
},
},
details: {
unhealthy: {
status: 'down',
},
},
});
});

it('should return a result object with errors with sync indicator function', async () => {
const result = await healthCheckExecutor.execute([
() => legacyUnhealthyIndicatorSync(),
]);
expect(result).toEqual<HealthCheckResult>({
status: 'error',
info: {},
error: {
unhealthy: {
status: 'down',
},
},
details: {
unhealthy: {
status: 'down',
},
},
});
});

it('should return a result object without errors and with errors', async () => {
const result = await healthCheckExecutor.execute([
() => legacyUnhealthyIndicator(),
() => legacyHealthyIndicator(),
]);
expect(result).toEqual<HealthCheckResult>({
status: 'error',
info: {
healthy: {
status: 'up',
},
},
error: {
unhealthy: {
status: 'down',
},
},
details: {
healthy: {
status: 'up',
},
unhealthy: {
status: 'down',
},
},
});
});

it('should return a result object with errors when error is not an instance of HealthCheckError', async () => {
const result = await healthCheckExecutor.execute([
() => legacyUnhealthyIndicatorWithoutError(),
]);
expect(result).toEqual<HealthCheckResult>({
status: 'error',
info: {},
error: {
unhealthy: {
status: 'down',
},
},
details: {
unhealthy: {
status: 'down',
},
},
});
});
});
});
});
11 changes: 9 additions & 2 deletions lib/health-check/health-check-executor.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,14 +65,21 @@ export class HealthCheckExecutor implements BeforeApplicationShutdown {

result.forEach((res) => {
if (res.status === 'fulfilled') {
results.push(res.value);
Object.entries(res.value).forEach(([key, value]) => {
if (value.status === 'up') {
results.push({ [key]: value });
} else if (value.status === 'down') {
errors.push({ [key]: value });
}
});
} else {
const error = res.reason;
// Is not an expected error. Throw further!
if (!isHealthCheckError(error)) {
throw error;
}
// Is a expected health check error

// eslint-disable-next-line deprecation/deprecation
errors.push((error as HealthCheckError).causes);
}
});
Expand Down
5 changes: 5 additions & 0 deletions lib/health-check/health-check.error.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
/**
* @deprecated
* This class has been deprecated and will be removed in the next major release.
* Instead utilise the `HealthIndicatorService` to indicate the health of your health indicator.
*/
export class HealthCheckError extends Error {
causes: any;
isHealthCheckError = true;
Expand Down
Loading

0 comments on commit 4a830f1

Please sign in to comment.