Skip to content

Commit

Permalink
Merge pull request #37 from nartc/mongo-health-check
Browse files Browse the repository at this point in the history
feature(@nestjs/terminus) Add health check for mongoose ODM
  • Loading branch information
BrunnerLivio authored Jan 8, 2019
2 parents 38bb1a2 + 1c7ba54 commit 5ea8171
Show file tree
Hide file tree
Showing 30 changed files with 9,091 additions and 172 deletions.
4 changes: 2 additions & 2 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ cache:
- "node_modules"

node_js:
- "10"
- "10"

env:
global:
Expand Down Expand Up @@ -45,4 +45,4 @@ deploy:
branch: master
node_js: "10"
tags: true
tag: beta
tag: beta
13 changes: 12 additions & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,26 @@ services:
networks:
- overlay

mongodb:
image: mongo:latest
hostname: mongodb
environment:
- MONGODB_DATABASE="test"
networks:
- overlay
ports:
- 27017:27017

lib:
build:
context: .
depends_on:
- mysql
- mongodb
networks:
- overlay
environment:
WAIT_HOSTS: mysql:3306
WAIT_HOSTS: mysql:3306, mongodb:27017

networks:
overlay:
12 changes: 4 additions & 8 deletions e2e/fasitfy.e2e-spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { INestApplication } from '@nestjs/common';
import { TerminusModuleOptions, TerminusEndpoint } from '../lib';

import Axios from 'axios';
import { TerminusEndpoint, TerminusModuleOptions } from '../lib';
import { bootstrapModule } from './helper/bootstrap-module';

describe('Fastify', () => {
Expand All @@ -18,13 +18,9 @@ describe('Fastify', () => {
},
];

[app, port] = await bootstrapModule(
{
useFactory: (): TerminusModuleOptions => ({ endpoints }),
},
false,
true,
);
[app, port] = await bootstrapModule({
useFactory: (): TerminusModuleOptions => ({ endpoints }),
});

// Workaraound to wait until module is bootsrapped
setTimeout(async () => {
Expand Down
13 changes: 5 additions & 8 deletions e2e/health-checks/dns.health.e2e-spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { INestApplication } from '@nestjs/common';
import { TerminusModuleOptions, DNSHealthIndicator } from '../../lib';

import Axios from 'axios';
import { DNSHealthIndicator, TerminusModuleOptions } from '../../lib';
import { bootstrapModule } from '../helper/bootstrap-module';

describe('DNS Health', () => {
Expand All @@ -22,13 +22,10 @@ describe('DNS Health', () => {
});

it('should check if google is available', async () => {
[app, port] = await bootstrapModule(
{
inject: [DNSHealthIndicator],
useFactory: getTerminusOptions,
},
false,
);
[app, port] = await bootstrapModule({
inject: [DNSHealthIndicator],
useFactory: getTerminusOptions,
});
const response = await Axios.get(`http://0.0.0.0:${port}/health`);
expect(response.status).toBe(200);
expect(response.data).toEqual({
Expand Down
76 changes: 76 additions & 0 deletions e2e/health-checks/mongoose.health.e2e-spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { INestApplication } from '@nestjs/common';

import Axios from 'axios';
import { MongooseHealthIndicator, TerminusModuleOptions } from '../../lib';
import { bootstrapModule } from '../helper/bootstrap-module';

describe('Mongoose Database Health', () => {
let app: INestApplication;
let port: number;

const getTerminusOptions = (
db: MongooseHealthIndicator,
): TerminusModuleOptions => ({
endpoints: [
{
url: '/health',
healthIndicators: [async () => db.pingCheck('mongo')],
},
],
});

it('should check if the mongoose is available', async () => {
[app, port] = await bootstrapModule(
{
inject: [MongooseHealthIndicator],
useFactory: getTerminusOptions,
},
false,
true,
);

const response = await Axios.get(`http://0.0.0.0:${port}/health`);
expect(response.status).toBe(200);
expect(response.data).toEqual({
status: 'ok',
info: { mongo: { status: 'up' } },
});
});

it('should throw an error if runs into timeout error', async () => {
[app, port] = await bootstrapModule(
{
inject: [MongooseHealthIndicator],
useFactory: (db: MongooseHealthIndicator): TerminusModuleOptions => ({
endpoints: [
{
url: '/health',
healthIndicators: [
async () => db.pingCheck('mongo', { timeout: 1 }),
],
},
],
}),
},
false,
true,
);

try {
await Axios.get(`http://0.0.0.0:${port}/health`, {});
} catch (error) {
expect(error.response.status).toBe(503);
expect(error.response.data).toEqual({
status: 'error',
error: {
mongo: {
status: 'down',
message: expect.any(String),
},
},
});
}
});

afterEach(async () => await app.close());
});
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { DatabaseHealthIndicator, TerminusModuleOptions } from '../../lib';
import Axios from 'axios';
import { bootstrapModule } from '../helper/bootstrap-module';

describe('Database Health', () => {
describe('TypeOrm Database Health', () => {
let app: INestApplication;
let port: number;

Expand All @@ -14,25 +14,26 @@ describe('Database Health', () => {
endpoints: [
{
url: '/health',
healthIndicators: [async () => db.pingCheck('database')],
healthIndicators: [async () => db.pingCheck('typeorm')],
},
],
});

it('should check if the database is available', async () => {
it('should check if the typeorm is available', async () => {
[app, port] = await bootstrapModule(
{
inject: [DatabaseHealthIndicator],
useFactory: getTerminusOptions,
},
true,
false,
);

const response = await Axios.get(`http://0.0.0.0:${port}/health`);
expect(response.status).toBe(200);
expect(response.data).toEqual({
status: 'ok',
info: { database: { status: 'up' } },
info: { typeorm: { status: 'up' } },
});
});

Expand All @@ -45,13 +46,14 @@ describe('Database Health', () => {
{
url: '/health',
healthIndicators: [
async () => db.pingCheck('database', { timeout: 1 }),
async () => db.pingCheck('typeorm', { timeout: 1 }),
],
},
],
}),
},
true,
false,
);

try {
Expand All @@ -61,7 +63,7 @@ describe('Database Health', () => {
expect(error.response.data).toEqual({
status: 'error',
error: {
database: {
typeorm: {
status: 'down',
message: expect.any(String),
},
Expand Down
18 changes: 14 additions & 4 deletions e2e/helper/bootstrap-module.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { TerminusModuleAsyncOptions, TerminusModule } from '../../lib';
import { DynamicModule, INestApplication } from '@nestjs/common';
import { NestFactory, FastifyAdapter } from '@nestjs/core';
import * as portfinder from 'portfinder';
import { FastifyAdapter, NestFactory } from '@nestjs/core';
import { TypeOrmModule } from '@nestjs/typeorm';
import { MongooseModule } from '@nestjs/mongoose';
import * as portfinder from 'portfinder';
import { TerminusModule, TerminusModuleAsyncOptions } from '../../lib';

const DbModule = TypeOrmModule.forRoot({
type: 'mysql',
Expand All @@ -16,16 +17,24 @@ const DbModule = TypeOrmModule.forRoot({
retryDelay: 1000,
});

const MongoModule = MongooseModule.forRoot('mongodb://mongodb:27017/test');

class ApplicationModule {
static forRoot(
options: TerminusModuleAsyncOptions,
useDb: boolean,
useMongoose: boolean,
): DynamicModule {
const imports = [TerminusModule.forRootAsync(options)];

if (useDb) {
imports.push(DbModule);
}

if (useMongoose) {
imports.push(MongoModule);
}

return {
module: ApplicationModule,
imports,
Expand All @@ -36,10 +45,11 @@ class ApplicationModule {
export async function bootstrapModule(
options: TerminusModuleAsyncOptions,
useDb: boolean = false,
useMongoose: boolean = false,
useFastify?: boolean,
): Promise<[INestApplication, number]> {
const app = await NestFactory.create(
ApplicationModule.forRoot(options, useDb),
ApplicationModule.forRoot(options, useDb, useMongoose),
useFastify ? new FastifyAdapter() : null,
);

Expand Down
2 changes: 1 addition & 1 deletion e2e/jest-e2e.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"json"
],
"transform": {
"^.+\\.tsx?$": "<rootDir>/../node_modules/ts-jest/preprocessor.js"
"^.+\\.tsx?$": "ts-jest"
},
"testRegex": "/e2e/.*\\.(e2e-test|e2e-spec).(ts|tsx|js)$",
"collectCoverageFrom": [
Expand Down
2 changes: 1 addition & 1 deletion jest.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"json"
],
"transform": {
"^.+\\.tsx?$": "<rootDir>/node_modules/ts-jest/preprocessor.js"
"^.+\\.tsx?$": "ts-jest"
},
"testRegex": "/lib/.*\\.(test|spec).(ts|tsx|js)$",
"collectCoverageFrom": [
Expand Down
2 changes: 1 addition & 1 deletion lib/errors/timeout-error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { HealthCheckError } from '@godaddy/terminus';

/**
* Gets thrown when the timeout of the
* database health check exceeds
* typeorm health check exceeds
*/
export class TimeoutError extends HealthCheckError {
/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { HealthCheckError } from '@godaddy/terminus';
import { Connection as MongooseConnection } from 'mongoose';
import { Connection } from 'typeorm';
import { ConnectionNotFoundError, TimeoutError } from '../../errors';
import { HealthIndicatorResult } from '../../interfaces';
import { TimeoutError as PromiseTimeoutError } from '../../utils';
import { DatabasePingCheckSettings } from '../databse-ping-check-settings.interface';
import { HealthIndicator } from './health-indicator';

/**
* Abstract AbstractDatabaseHealthIndicator
*/
export abstract class AbstractDatabaseHealthIndicator extends HealthIndicator {
/**
* Constructor with the connection
* @param connection The underlying Connection instance from TypeOrm or Mongoose connection
*/
protected constructor(protected connection: Connection | MongooseConnection) {
super();
}

/**
* Pings a typeorm
* @param connection The connection which the ping should get executed
* @param timeout The timeout how long the ping should maximum take
*/
protected abstract async pingDb(
connection: Connection | MongooseConnection,
timeout: number,
): Promise<any>;

/**
* Checks if the typeorm responds in (default) 1000ms and
* returns a result object corresponding to the result
* @param key
* @param options
*/
async pingCheck(
key: string,
options: DatabasePingCheckSettings = {},
): Promise<HealthIndicatorResult> {
let isHealthy = false;
const connection = options.connection || this.connection;
const timeout = options.timeout || 1000;

if (!connection) {
throw new ConnectionNotFoundError(
this.getStatus(key, isHealthy, {
message: '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`,
}),
);
}
}

if (isHealthy) {
return this.getStatus(key, isHealthy);
} else {
throw new HealthCheckError(
`${key} is not available`,
this.getStatus(key, isHealthy),
);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { HealthIndicatorResult } from '../';
import { HealthIndicatorResult } from '../..';

/**
* Represents an abstract health indicator
Expand Down
Loading

0 comments on commit 5ea8171

Please sign in to comment.