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

feat(health-indicator): add mikro-orm health indicator #1876

Closed
wants to merge 4 commits into from
Closed
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
53 changes: 53 additions & 0 deletions e2e/health-checks/mikro-orm.health.e2e-spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import * as request from 'supertest';
import { INestApplication } from '@nestjs/common';
import { bootstrapTestingModule, DynamicHealthEndpointFn } from '../helper';

describe('MikroOrmHealthIndicator', () => {
let app: INestApplication;
let setHealthEndpoint: DynamicHealthEndpointFn;

beforeEach(
() =>
(setHealthEndpoint =
bootstrapTestingModule().withMikroOrm().setHealthEndpoint),
);

describe('#pingCheck', () => {
it('should check if the mikroOrm is available', async () => {
app = await setHealthEndpoint(({ healthCheck, mikroOrm }) =>
healthCheck.check([async () => mikroOrm.pingCheck('mikroOrm')]),
).start();
const details = { mikroOrm: { status: 'up' } };
return request(app.getHttpServer()).get('/health').expect(200).expect({
status: 'ok',
info: details,
error: {},
details,
});
});

it('should throw an error if runs into timeout error', async () => {
app = await setHealthEndpoint(({ healthCheck, mikroOrm }) =>
healthCheck.check([
async () => mikroOrm.pingCheck('mikroOrm', { timeout: 1 }),
]),
).start();

const details = {
mikroOrm: {
status: 'down',
message: 'timeout of 1ms exceeded',
},
};

return request(app.getHttpServer()).get('/health').expect(503).expect({
status: 'error',
info: {},
error: details,
details,
});
});
});

afterEach(async () => await app.close());
});
23 changes: 23 additions & 0 deletions e2e/helper/bootstrap-testing-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ import {
import { SequelizeModule } from '@nestjs/sequelize';
import { MongooseModule } from '@nestjs/mongoose';
import { HttpModule } from '@nestjs/axios';
import { MikroOrmHealthIndicator } from '../../lib/health-indicator/database/mikro-orm.health';
import { MikroOrmModule } from '@mikro-orm/nestjs';

type TestingHealthFunc = (props: {
healthCheck: HealthCheckService;
Expand All @@ -34,6 +36,7 @@ type TestingHealthFunc = (props: {
mongoose: MongooseHealthIndicator;
sequelize: SequelizeHealthIndicator;
typeorm: TypeOrmHealthIndicator;
mikroOrm: MikroOrmHealthIndicator;
}) => Promise<HealthCheckResult>;

function createHealthController(func: TestingHealthFunc) {
Expand All @@ -48,6 +51,7 @@ function createHealthController(func: TestingHealthFunc) {
private readonly mongoose: MongooseHealthIndicator,
private readonly sequelize: SequelizeHealthIndicator,
private readonly typeorm: TypeOrmHealthIndicator,
private readonly mikroOrm: MikroOrmHealthIndicator,
) {}
// @ts-ignore
@Get('health')
Expand All @@ -61,6 +65,7 @@ function createHealthController(func: TestingHealthFunc) {
mongoose: this.mongoose,
sequelize: this.sequelize,
typeorm: this.typeorm,
mikroOrm: this.mikroOrm,
});
}
}
Expand Down Expand Up @@ -140,6 +145,23 @@ export function bootstrapTestingModule() {
return { setHealthEndpoint };
}

function withMikroOrm() {
imports.push(
MikroOrmModule.forRoot({
type: 'mysql',
host: '0.0.0.0',
port: 3306,
user: 'root',
password: 'root',
dbName: 'test',
discovery: { warnWhenNoEntities: false },
strict: true,
}),
);

return { setHealthEndpoint };
}

function withHttp() {
imports.push(HttpModule);
return { setHealthEndpoint };
Expand All @@ -150,6 +172,7 @@ export function bootstrapTestingModule() {
withTypeOrm,
withSequelize,
withHttp,
withMikroOrm,
setHealthEndpoint,
};
}
139 changes: 139 additions & 0 deletions lib/health-indicator/database/mikro-orm.health.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import { Injectable, NotImplementedException, Scope } from '@nestjs/common';
import { ModuleRef } from '@nestjs/core';
import { HealthCheckError } from '../../health-check/health-check.error';

import { Connection, MikroORM } from '@mikro-orm/core';
import { TimeoutError } from '../../errors';
import {
TimeoutError as PromiseTimeoutError,
promiseTimeout,
checkPackages,
} from '../../utils';
import { HealthIndicator, HealthIndicatorResult } from '..';

export interface MikroOrmPingCheckSettings {
/**
* The connection which the ping check should get executed
*/
connection?: any;
/**
* The amount of time the check should require in ms
*/
timeout?: number;
}

/**
* The MikroOrmHealthIndicator contains health indicators
* which are used for health checks related to MikroOrm
*
* @publicApi
* @module TerminusModule
*/
@Injectable({ scope: Scope.TRANSIENT })
export class MikroOrmHealthIndicator extends HealthIndicator {
/**
* Initializes the MikroOrmHealthIndicator
*
* @param {ModuleRef} moduleRef The NestJS module reference
*/
constructor(private moduleRef: ModuleRef) {
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`,
}),
);
}

// Check if the error is a connection not found error
if (error instanceof Error) {
throw new HealthCheckError(
error.message,
this.getStatus(key, false, {
message: error.message,
}),
);
}
}

return this.getStatus(key, true);
}

private checkDependantPackages() {
checkPackages(
['@mikro-orm/nestjs', '@mikro-orm/core'],
this.constructor.name,
);
}

/**
* Returns the connection of the current DI context
*/
private getContextConnection(): Connection | null {
const mikro = this.moduleRef.get(MikroORM, { strict: false });

const connection: Connection = mikro.em.getConnection();

if (!connection) {
return null;
}
return connection;
}

/**
* Pings a mikro-orm connection
*
* @param connection The connection which the ping should get executed
* @param timeout The timeout how long the ping should maximum take
*
*/
private async pingDb(connection: Connection, timeout: number) {
let check: Promise<any>;
const type = connection.getPlatform().getConfig().get('type');

switch (type) {
case 'postgresql':
case 'mysql':
jbw marked this conversation as resolved.
Show resolved Hide resolved
case 'mariadb':
case 'sqlite':
check = connection.execute('SELECT 1');
break;
default:
throw new NotImplementedException(
`${type} ping check is not implemented yet`,
);
}
return await promiseTimeout(timeout, check);
}
}
2 changes: 2 additions & 0 deletions lib/health-indicator/health-indicators.provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
HealthIndicator,
GRPCHealthIndicator,
} from '.';
import { MikroOrmHealthIndicator } from './database/mikro-orm.health';

/**
* All the health indicators terminus provides as array
Expand All @@ -24,4 +25,5 @@ export const HEALTH_INDICATORS: Type<HealthIndicator>[] = [
MemoryHealthIndicator,
MicroserviceHealthIndicator,
GRPCHealthIndicator,
MikroOrmHealthIndicator,
];
Loading