Skip to content

Commit

Permalink
feat(@nestjs/terminus): Add microservice health indicator
Browse files Browse the repository at this point in the history
  • Loading branch information
BrunnerLivio committed Mar 10, 2019
1 parent 5ea8171 commit 7cc931a
Show file tree
Hide file tree
Showing 18 changed files with 11,530 additions and 3,561 deletions.
8 changes: 8 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,14 @@ services:
ports:
- 27017:27017

redis_test:
image: redis:latest
hostname: redis_test
networks:
- overlay
ports:
- 6379:6379

lib:
build:
context: .
Expand Down
102 changes: 102 additions & 0 deletions e2e/health-checks/microservice.health.e2e-spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { INestApplication } from '@nestjs/common';

import Axios from 'axios';
import { TerminusModuleOptions, MicroserviceHealthIndicator } from '../../lib';
import { bootstrapModule } from '../helper/bootstrap-module';
import { Transport } from '@nestjs/microservices';

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

const getTerminusOptions = (
microservice: MicroserviceHealthIndicator,
): TerminusModuleOptions => {
const tcpCheck = async () =>
microservice.pingCheck('tcp', {
transport: Transport.TCP,
options: {
host: '0.0.0.0',
port: 8890,
},
});

return {
endpoints: [
{
url: '/health',
healthIndicators: [tcpCheck],
},
],
};
};

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

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

it('should throw an error if runs into timeout error', async () => {
[app, port] = await bootstrapModule(
{
inject: [MicroserviceHealthIndicator],
useFactory: (
microservice: MicroserviceHealthIndicator,
): TerminusModuleOptions => ({
endpoints: [
{
url: '/health',
healthIndicators: [
async () =>
microservice.pingCheck('tcp', {
timeout: 1,
transport: Transport.TCP,
options: {
host: '0.0.0.0',
port: 8889,
},
}),
],
},
],
}),
},
false,
false,
false,
8889,
);

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: {
tcp: {
status: 'down',
message: expect.any(String),
},
},
});
}
});

afterEach(async () => await app.close());
});
18 changes: 18 additions & 0 deletions e2e/helper/bootstrap-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { TypeOrmModule } from '@nestjs/typeorm';
import { MongooseModule } from '@nestjs/mongoose';
import * as portfinder from 'portfinder';
import { TerminusModule, TerminusModuleAsyncOptions } from '../../lib';
import { Transport } from '@nestjs/microservices';

const DbModule = TypeOrmModule.forRoot({
type: 'mysql',
Expand Down Expand Up @@ -42,17 +43,34 @@ class ApplicationModule {
}
}

async function bootstrapMicroservice(tcpPort: number) {
const tcpApp = await NestFactory.createMicroservice(ApplicationModule, {
transport: Transport.TCP,
options: {
host: '0.0.0.0',
port: tcpPort,
},
});

await tcpApp.listenAsync();
}

export async function bootstrapModule(
options: TerminusModuleAsyncOptions,
useDb: boolean = false,
useMongoose: boolean = false,
useFastify?: boolean,
tcpPort?: number,
): Promise<[INestApplication, number]> {
const app = await NestFactory.create(
ApplicationModule.forRoot(options, useDb, useMongoose),
useFastify ? new FastifyAdapter() : null,
);

if (tcpPort) {
await bootstrapMicroservice(tcpPort);
}

const port = await portfinder.getPortPromise({
port: 3000,
stopPort: 8888,
Expand Down
1 change: 1 addition & 0 deletions lib/health-indicators/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './typeorm/typeorm.health';
export * from './dns/dns.health';
export * from './mongoose/mongoose.health';
export * from './microservice/microservice.health';
95 changes: 95 additions & 0 deletions lib/health-indicators/microservice/microservice.health.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { Injectable } from '@nestjs/common';
import { HealthIndicatorResult } from '../../interfaces';
import { HealthIndicator } from '../abstract/health-indicator';
import { HealthCheckError } from '@godaddy/terminus';
import { ClientProxyFactory, ClientOptions } from '@nestjs/microservices';
import {
promiseTimeout,
TimeoutError as PromiseTimeoutError,
} from '../../utils';
import { TimeoutError } from '../../errors';

export type MicroserviceHealthIndicatorOptions = ClientOptions & {
timeout?: number;
};

/**
* The MicroserviceHealthIndicator is a health indicators
* which is used for health checks related to microservices
*/
@Injectable()
export class MicroserviceHealthIndicator extends HealthIndicator {
/**
* Initializes the health indicator
*/
constructor() {
super();
}

private async pingMicroservice(
options: MicroserviceHealthIndicatorOptions,
): Promise<any> {
const client = ClientProxyFactory.create(options);
return await client.connect();
}

/**
* Prepares and throw a HealthCheckError
* @param key The key which will be used for the result object
* @param error The thrown error
* @param timeout The timeout in ms
*
* @throws {HealthCheckError}
*/
private generateError(key: string, error: Error, timeout: number) {
if (!error) {
return;
}
if (error instanceof PromiseTimeoutError) {
throw new TimeoutError(
timeout,
this.getStatus(key, false, {
message: `timeout of ${timeout}ms exceeded`,
}),
);
}
throw new HealthCheckError(
error.message,
this.getStatus(key, false, {
message: error.message,
}),
);
}

/**
* Checks if the given microservice is up
* @param key The key which will be used for the result object
* @param options The options of the microservice
*
* @throws {HealthCheckError} If the microservice is not reachable
*
* @example
* ```TypeScript
* microservice.pingCheck('tcp', {
* transport: Transport.TCP,
* options: { host: 'localhost', port: 3001 },
* })
* ```
*/
async pingCheck(
key: string,
options: MicroserviceHealthIndicatorOptions,
): Promise<HealthIndicatorResult> {
let isHealthy = false;
const timeout = options.timeout || 1000;

try {
await promiseTimeout(timeout, this.pingMicroservice(options));
isHealthy = true;
} catch (err) {
this.generateError(key, err, timeout);
}

return this.getStatus(key, isHealthy);
}
}
15 changes: 13 additions & 2 deletions lib/terminus-core.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@ import { TerminusBootstrapService } from './terminus-bootstrap.service';
import { TerminusLibProvider } from './terminus-lib.provider';
import { TerminusModule } from './terminus.module';
import { DatabaseHealthIndicator, MongooseHealthIndicator } from '.';
import { DNSHealthIndicator } from './health-indicators';
import {
DNSHealthIndicator,
MicroserviceHealthIndicator,
} from './health-indicators';

/**
* The internal Terminus Module which handles the integration
Expand Down Expand Up @@ -47,8 +50,14 @@ export class TerminusCoreModule {
TerminusBootstrapService,
DatabaseHealthIndicator,
MongooseHealthIndicator,
MicroserviceHealthIndicator,
],
exports: [
DatabaseHealthIndicator,
MongooseHealthIndicator,
DNSHealthIndicator,
MicroserviceHealthIndicator,
],
exports: [DatabaseHealthIndicator, MongooseHealthIndicator],
};
}

Expand All @@ -69,11 +78,13 @@ export class TerminusCoreModule {
DatabaseHealthIndicator,
DNSHealthIndicator,
MongooseHealthIndicator,
MicroserviceHealthIndicator,
],
exports: [
DatabaseHealthIndicator,
DNSHealthIndicator,
MongooseHealthIndicator,
MicroserviceHealthIndicator,
],
};
}
Expand Down
Loading

0 comments on commit 7cc931a

Please sign in to comment.