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

Create HealthIndicatorService to simplify custom health indicators #2510

Draft
wants to merge 6 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all 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
28 changes: 28 additions & 0 deletions lib/health-check/health-check-executor.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,14 @@ const unhealthyCheckSync = () => {
});
};

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

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

Expand Down Expand Up @@ -125,5 +133,25 @@ describe('HealthCheckExecutorService', () => {
},
});
});

it('should return a result object with errors when error is not an instance of HealthCheckError', async () => {
const result = await healthCheckExecutor.execute([
() => unhealthyCheckWithoutError(),
]);
expect(result).toEqual<HealthCheckResult>({
status: 'error',
info: {},
error: {
unhealthy: {
status: 'down',
},
},
details: {
unhealthy: {
status: 'down',
},
},
});
});
});
});
10 changes: 8 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,20 @@ 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

errors.push((error as HealthCheckError).causes);
}
});
Expand Down
105 changes: 45 additions & 60 deletions lib/health-indicator/database/mikro-orm.health.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,13 @@ import type * as MikroOrm from '@mikro-orm/core';
import { Injectable, Scope } from '@nestjs/common';
import { ModuleRef } from '@nestjs/core';
import { HealthIndicator, type HealthIndicatorResult } from '..';
import { TimeoutError } from '../../errors';
import { DatabaseNotConnectedError } from '../../errors/database-not-connected.error';
import { HealthCheckError } from '../../health-check/health-check.error';
import {
TimeoutError as PromiseTimeoutError,
promiseTimeout,
checkPackages,
} from '../../utils';
import { HealthIndicatorService } from '../health-indicator.service';

export interface MikroOrmPingCheckSettings {
/**
Expand All @@ -31,68 +30,14 @@ export interface MikroOrmPingCheckSettings {
*/
@Injectable({ scope: Scope.TRANSIENT })
export class MikroOrmHealthIndicator extends HealthIndicator {
/**
* Initializes the MikroOrmHealthIndicator
*
* @param {ModuleRef} moduleRef The NestJS module reference
*/
constructor(private moduleRef: ModuleRef) {
constructor(
private readonly moduleRef: ModuleRef,
private readonly healthIndicatorService: HealthIndicatorService,
) {
super();
this.checkDependantPackages();
}

/**
* Checks if responds in (default) 1000ms and
* returns a result object corresponding to the result
* @param key The key which will be used for the result object
* @param options The options for the ping
*
* @example
* MikroOrmHealthIndicator.pingCheck('database', { timeout: 1500 });
*/
public async pingCheck(
key: string,
options: MikroOrmPingCheckSettings = {},
): Promise<HealthIndicatorResult> {
this.checkDependantPackages();

const connection = options.connection || this.getContextConnection();
const timeout = options.timeout || 1000;

if (!connection) {
return this.getStatus(key, false);
}

try {
await this.pingDb(connection, timeout);
} catch (error) {
// Check if the error is a timeout error
if (error instanceof PromiseTimeoutError) {
throw new TimeoutError(
timeout,
this.getStatus(key, false, {
message: `timeout of ${timeout}ms exceeded`,
}),
);
}
if (error instanceof DatabaseNotConnectedError) {
throw new HealthCheckError(
error.message,
this.getStatus(key, false, {
message: error.message,
}),
);
}

throw new HealthCheckError(
`${key} is not available`,
this.getStatus(key, false),
);
}

return this.getStatus(key, true);
}

private checkDependantPackages() {
checkPackages(
['@mikro-orm/nestjs', '@mikro-orm/core'],
Expand Down Expand Up @@ -133,4 +78,44 @@ export class MikroOrmHealthIndicator extends HealthIndicator {

return await promiseTimeout(timeout, checker());
}

/**
* Checks if responds in (default) 1000ms and
* returns a result object corresponding to the result
* @param key The key which will be used for the result object
* @param options The options for the ping
*
* @example
* MikroOrmHealthIndicator.pingCheck('database', { timeout: 1500 });
*/
public async pingCheck<Key extends string = string>(
key: Key,
options: MikroOrmPingCheckSettings = {},
): Promise<HealthIndicatorResult<Key>> {
this.checkDependantPackages();
const check = this.healthIndicatorService.check(key);

const timeout = options.timeout || 1000;
const connection = options.connection || this.getContextConnection();

if (!connection) {
return check.down();
}

try {
await this.pingDb(connection, timeout);
} catch (error) {
// Check if the error is a timeout error
if (error instanceof PromiseTimeoutError) {
return check.down(`timeout of ${timeout}ms exceeded`);
}
if (error instanceof DatabaseNotConnectedError) {
return check.down(error.message);
}

return check.down();
}

return check.up();
}
}
51 changes: 15 additions & 36 deletions lib/health-indicator/database/mongoose.health.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,14 @@
import { Injectable, Scope } from '@nestjs/common';
import { ModuleRef } from '@nestjs/core';
import type * as NestJSMongoose from '@nestjs/mongoose';
import {
type HealthIndicatorResult,
TimeoutError,
ConnectionNotFoundError,
} from '../..';
import { HealthCheckError } from '../../health-check/health-check.error';
import { type HealthIndicatorResult } from '../..';
import {
promiseTimeout,
TimeoutError as PromiseTimeoutError,
checkPackages,
} from '../../utils';
import { HealthIndicator } from '../health-indicator';
import { HealthIndicatorService } from '../health-indicator.service';

export interface MongoosePingCheckSettings {
/**
Expand All @@ -34,12 +30,10 @@ export interface MongoosePingCheckSettings {
*/
@Injectable({ scope: Scope.TRANSIENT })
export class MongooseHealthIndicator extends HealthIndicator {
/**
* Initializes the MongooseHealthIndicator
*
* @param {ModuleRef} moduleRef The NestJS module reference
*/
constructor(private moduleRef: ModuleRef) {
constructor(
private readonly moduleRef: ModuleRef,
private readonly healthIndicatorService: HealthIndicatorService,
) {
super();
this.checkDependantPackages();
}
Expand Down Expand Up @@ -92,45 +86,30 @@ export class MongooseHealthIndicator extends HealthIndicator {
* @example
* mongooseHealthIndicator.pingCheck('mongodb', { timeout: 1500 });
*/
public async pingCheck(
key: string,
public async pingCheck<Key extends string = string>(
key: Key,
options: MongoosePingCheckSettings = {},
): Promise<HealthIndicatorResult> {
let isHealthy = false;
): Promise<HealthIndicatorResult<Key>> {
this.checkDependantPackages();
const check = this.healthIndicatorService.check(key);

const connection = options.connection || this.getContextConnection();
const timeout = options.timeout || 1000;

if (!connection) {
throw new ConnectionNotFoundError(
this.getStatus(key, isHealthy, {
message: 'Connection provider not found in application context',
}),
);
return check.down('Connection provider not found in application context');
}

try {
await this.pingDb(connection, timeout);
isHealthy = true;
} catch (err) {
if (err instanceof PromiseTimeoutError) {
throw new TimeoutError(
timeout,
this.getStatus(key, isHealthy, {
message: `timeout of ${timeout}ms exceeded`,
}),
);
return check.down(`timeout of ${timeout}ms exceeded`);
}
}

if (isHealthy) {
return this.getStatus(key, isHealthy);
} else {
throw new HealthCheckError(
`${key} is not available`,
this.getStatus(key, isHealthy),
);
return check.down();
}

return check.up();
}
}
33 changes: 11 additions & 22 deletions lib/health-indicator/database/prisma.health.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { Injectable } from '@nestjs/common';
import { TimeoutError } from '../../errors';
import { HealthCheckError } from '../../health-check';
import {
promiseTimeout,
TimeoutError as PromiseTimeoutError,
} from '../../utils';
import { HealthIndicator } from '../health-indicator';
import { type HealthIndicatorResult } from '../health-indicator-result.interface';
import { HealthIndicatorService } from '../health-indicator.service';

type PingCommandSignature = { [Key in string]?: number };

Expand Down Expand Up @@ -35,7 +35,7 @@ export interface PrismaClientPingCheckSettings {
*/
@Injectable()
export class PrismaHealthIndicator extends HealthIndicator {
constructor() {
constructor(private readonly healthIndicatorService: HealthIndicatorService) {
super();
}

Expand Down Expand Up @@ -69,35 +69,24 @@ export class PrismaHealthIndicator extends HealthIndicator {
* @param prismaClient PrismaClient
* @param options The options for the ping
*/
public async pingCheck(
key: string,
public async pingCheck<Key extends string = string>(
key: Key,
prismaClient: PrismaClient,
options: PrismaClientPingCheckSettings = {},
): Promise<any> {
let isHealthy = false;
): Promise<HealthIndicatorResult<Key>> {
const check = this.healthIndicatorService.check(key);
const timeout = options.timeout || 1000;

try {
await this.pingDb(timeout, prismaClient);
isHealthy = true;
} catch (error) {
if (error instanceof PromiseTimeoutError) {
throw new TimeoutError(
timeout,
this.getStatus(key, isHealthy, {
message: `timeout of ${timeout}ms exceeded`,
}),
);
return check.down(`timeout of ${timeout}ms exceeded`);
}
}

if (isHealthy) {
return this.getStatus(key, isHealthy);
} else {
throw new HealthCheckError(
`${key} is not available`,
this.getStatus(key, isHealthy),
);
return check.down();
}

return check.up();
}
}
Loading
Loading