Skip to content

Commit

Permalink
feat(database): Add database health indicator
Browse files Browse the repository at this point in the history
  • Loading branch information
BrunnerLivio committed Nov 2, 2018
1 parent fceb4ba commit bdd4652
Show file tree
Hide file tree
Showing 24 changed files with 940 additions and 55 deletions.
17 changes: 10 additions & 7 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,20 +1,27 @@

language: node_js

services:
- docker

cache:
directories:
- "node_modules"

node_js:
- "8"
- "9"
- "10"

env:
global:
- "GH_REF=github.com/BrunnerLivio/nest-terminus.git"

before_install:
- npm i -g npm@latest

install:
- npm install
script:
- npm run test
- docker-compose build && docker-compose up
deploy:
- provider: script
# Have to use `&&` because of issue https://github.com/travis-ci/dpl/issues/673
Expand All @@ -31,7 +38,3 @@ deploy:
node_js: "10"
tags: true
tag: beta

env:
global:
- "GH_REF=github.com/BrunnerLivio/nest-terminus.git"
11 changes: 11 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
FROM node:latest

WORKDIR /usr/src/app

COPY package*.json ./

RUN npm install

COPY . .

CMD ["npm", "test"]
12 changes: 12 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
version: "3"

services:

lib:
build:
context: .
networks:
- overlay

networks:
overlay:
99 changes: 99 additions & 0 deletions e2e/health-checks/database.health.e2e-spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { INestApplication, DynamicModule } from '@nestjs/common';
import { Test } from '@nestjs/testing';
import { TerminusOptions } from '@godaddy/terminus';
import {
DatabaseHealthIndicator,
TerminusModuleOptions,
TerminusModule,
} from '../../lib';
import { HTTP_SERVER_REF, NestFactory } from '@nestjs/core';
import * as http from 'http';

import { TypeOrmModule } from '@nestjs/typeorm';
import Axios from 'axios';

describe('Database Health', () => {
let app: INestApplication;
const PORT = process.env.PORT || 3001;

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

class ApplicationModule {
static forRoot(options): DynamicModule {
return {
module: ApplicationModule,
imports: [
TypeOrmModule.forRoot({
type: 'sqlite',
database: 'test',
keepConnectionAlive: true,
}),
TerminusModule.forRootAsync(options),
],
};
}
}

async function bootstrapModule(options) {
app = await NestFactory.create(ApplicationModule.forRoot(options));
await app.listen(PORT);
}

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

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' } },
});
});

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

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: {
database: {
status: 'down',
message: 'Database did not respond after 1ms',
},
},
});
}
});

afterEach(async () => {
app.close();
});
});
3 changes: 2 additions & 1 deletion e2e/jest-e2e.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,6 @@
"coverageReporters": [
"json",
"lcov"
]
],
"testEnvironment": "node"
}
4 changes: 3 additions & 1 deletion e2e/terminus.e2e-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ describe('Terminus', () => {
endpoints: [
{
url: '/health',
healthIndicators: [async () => ({ key: true })],
healthIndicators: [
async () => ({ db: { whatever: true, status: 'up' } }),
],
},
],
};
Expand Down
39 changes: 20 additions & 19 deletions jest.json
Original file line number Diff line number Diff line change
@@ -1,21 +1,22 @@
{
"moduleFileExtensions": [
"ts",
"tsx",
"js",
"json"
],
"transform": {
"^.+\\.tsx?$": "<rootDir>/node_modules/ts-jest/preprocessor.js"
},
"testRegex": "/src/.*\\.(test|spec).(ts|tsx|js)$",
"collectCoverageFrom": [
"src/**/*.{js,jsx,tsx,ts}",
"!**/node_modules/**",
"!**/vendor/**"
],
"coverageReporters": [
"json",
"lcov"
]
"moduleFileExtensions": [
"ts",
"tsx",
"js",
"json"
],
"transform": {
"^.+\\.tsx?$": "<rootDir>/node_modules/ts-jest/preprocessor.js"
},
"testRegex": "/src/.*\\.(test|spec).(ts|tsx|js)$",
"collectCoverageFrom": [
"src/**/*.{js,jsx,tsx,ts}",
"!**/node_modules/**",
"!**/vendor/**"
],
"coverageReporters": [
"json",
"lcov"
],
"testEnvironment": "node"
}
7 changes: 7 additions & 0 deletions lib/health-indicators/database/connection-not-found.error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { HealthCheckError } from '@godaddy/terminus';

export class ConnectionNotFoundError extends HealthCheckError {
constructor(cause) {
super('Connection provider not found in application context', cause);
}
}
73 changes: 73 additions & 0 deletions lib/health-indicators/database/database.health.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { Injectable, Optional } from '@nestjs/common';
import { HealthIndicatorResult } from '../..';
import { Connection } from 'typeorm';
import { HealthCheckError } from '@godaddy/terminus';
import { ConnectionNotFoundError } from './connection-not-found.error';
import {
promiseTimeout,
TimeoutError as PromiseTimeoutError,
} from '../../utils';
import { TimeoutError } from './timeout-error';

export interface DatabasePingCheckSettings {
connection?: Connection;
timeout?: number;
}

@Injectable()
export class DatabaseHealthIndicator {
constructor(@Optional() private readonly connection: Connection) {}

private getStatus(key: string, isHealthy: boolean, options?: any) {
return {
[key]: {
status: isHealthy ? 'up' : 'down',
...options,
},
};
}

private async pingDb(connection, timeout) {
return await promiseTimeout(timeout, connection.query('SELECT 1'));
}

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: `Database did not respond after ${timeout}ms`,
}),
);
}
}

if (isHealthy) {
return this.getStatus(key, isHealthy);
} else {
throw new HealthCheckError(
'Database is not available',
this.getStatus(key, isHealthy),
);
}
}
}
7 changes: 7 additions & 0 deletions lib/health-indicators/database/timeout-error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { HealthCheckError } from '@godaddy/terminus';

export class TimeoutError extends HealthCheckError {
constructor(timeout, cause) {
super(`Database did not respond after ${timeout}ms`, cause);
}
}
2 changes: 2 additions & 0 deletions lib/health-indicators/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './database/database.health';
export * from './database/connection-not-found.error';
1 change: 1 addition & 0 deletions lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './interfaces';
export * from './terminus.module';
export * from './health-indicators';
18 changes: 4 additions & 14 deletions lib/interfaces/health-indicator.interface.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,8 @@
export type HealthIndicatorResult = {
[key: string]: any;
[key: string]: {
status: string;
[optionalKeys: string]: any;
};
};

export type HealthIndicatorFunction = () => Promise<HealthIndicatorResult>;

/**
* Represents a health indicator of a health check
*/
export interface HealthIndicator {
/**
* If the health indicator is healthy
*
* @param {string} key The key of the health check which will be used in the result object
* @param {any} [options] The options to configure the health indicator
*/
isHealthy(key: string, options?: any): Promise<HealthIndicatorResult>;
}
4 changes: 2 additions & 2 deletions lib/terminus-bootstrap.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
HttpServer,
} from '@nestjs/common';
import { TERMINUS_MODULE_OPTIONS, TERMINUS_LIB } from './terminus.constants';
import { TerminusModuleOptions, HealthIndicator } from './interfaces';
import { TerminusModuleOptions } from './interfaces';
import { HTTP_SERVER_REF } from '@nestjs/core';
import { Server } from 'http';
import { HealthCheckError, Terminus } from '@godaddy/terminus';
Expand Down Expand Up @@ -44,7 +44,7 @@ export class TerminusBootstrapService implements OnApplicationBootstrap {
): Promise<{ results: any[]; errors: any[] }> {
const results: any[] = [];
const errors: any[] = [];
await Promise.all<HealthIndicator>(
await Promise.all(
healthIndicators
// Register all promises
.map(healthIndicator => healthIndicator())
Expand Down
5 changes: 5 additions & 0 deletions lib/terminus-core.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { TERMINUS_MODULE_OPTIONS } from './terminus.constants';
import { TerminusBootstrapService } from './terminus-bootstrap.service';
import { TerminusLibProvider } from './terminus-lib.provider';
import { TerminusModule } from './terminus.module';
import { DatabaseHealthIndicator } from '.';

/**
* The internal Terminus Module which handles the integration
Expand Down Expand Up @@ -49,7 +50,9 @@ export class TerminusCoreModule {
terminusModuleOptions,
TerminusLibProvider,
TerminusBootstrapService,
DatabaseHealthIndicator,
],
exports: [DatabaseHealthIndicator],
};
}

Expand All @@ -67,7 +70,9 @@ export class TerminusCoreModule {
...asyncProviders,
TerminusBootstrapService,
TerminusLibProvider,
DatabaseHealthIndicator,
],
exports: [DatabaseHealthIndicator],
};
}

Expand Down
Loading

0 comments on commit bdd4652

Please sign in to comment.