Skip to content

Commit

Permalink
feat: add Healthcheck module
Browse files Browse the repository at this point in the history
  • Loading branch information
thetutlage committed Jul 27, 2019
1 parent 9f6a8a6 commit df92dee
Show file tree
Hide file tree
Showing 5 changed files with 328 additions and 0 deletions.
23 changes: 23 additions & 0 deletions adonis-typings/application.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*
* @adonisjs/core
*
* (c) Harminder Virk <virk@adonisjs.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

/**
* The binding for the given module is defined inside `providers/AppProvider.ts`
* file.
*/
declare module '@ioc:Adonis/Core/Application' {
import { ApplicationContract as BaseContract } from '@poppinss/application'
const Application: ApplicationContract

/**
* Module exports
*/
export interface ApplicationContract extends BaseContract {}
export default Application
}
48 changes: 48 additions & 0 deletions adonis-typings/health-check.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
* @adonisjs/core
*
* (c) Harminder Virk <virk@adonisjs.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

/**
* The binding for the given module is defined inside `providers/AppProvider.ts`
* file.
*/
declare module '@ioc:Adonis/Core/HealtCheck' {
export type Checker = string | (() => Promise<HealthReportEntry>)

/**
* Shape of health report entry. Each checker must
* return an object with similar shape.
*/
export type HealthReportEntry = {
health: {
healthy: boolean,
message?: string,
},
meta?: any,
}

/**
* The shape of entire report
*/
export type HealthReport = {
[service: string]: HealthReportEntry,
}

/**
* Shape of health check contract
*/
export interface HealthCheckContract {
addChecker (service: string, checker: Checker): void,
isLive (): Promise<boolean>,
isReady (): boolean,
report (): Promise<{ healthy: boolean, report: HealthReport }>,
}

const HealthCheck: HealthCheckContract
export default HealthCheck
}
3 changes: 3 additions & 0 deletions adonis-typings/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,15 @@
* file that was distributed with this source code.
*/

/// <reference path="./application.ts" />
/// <reference path="./config.ts" />
/// <reference path="./cors.ts" />
/// <reference path="./encryption.ts" />
/// <reference path="./env.ts" />
/// <reference path="./event.ts" />
/// <reference path="./exception-handler.ts" />
/// <reference path="./hash.ts" />
/// <reference path="./health-check.ts" />
/// <reference path="./http-context.ts" />
/// <reference path="./logger.ts" />
/// <reference path="./middleware-store.ts" />
Expand Down
98 changes: 98 additions & 0 deletions src/HealthCheck/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/*
* @adonisjs/core
*
* (c) Harminder Virk <virk@adonisjs.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

import { ApplicationContract } from '@ioc:Adonis/Core/Application'
import { parseIocReference, callIocReference } from '@poppinss/utils'
import { HealthCheckContract, Checker, HealthReport, HealthReportEntry } from '@ioc:Adonis/Core/HealtCheck'

/**
* The module exposes the API to find the health, liveliness and readiness of
* the system. You can also add your own checkers.
*/
export class HealthCheck implements HealthCheckContract {
/**
* A copy of custom checkers
*/
private _healthCheckers: { [service: string]: Checker } = {}

constructor (private _application: ApplicationContract) {}

/**
* Invokes a given checker to collect the report metrics.
*/
private async _invokeChecker (
service: string,
reportSheet: HealthReport,
): Promise<boolean> {
const checker = this._healthCheckers[service]
let report: HealthReportEntry

try {
if (typeof (checker) === 'function') {
report = await checker()
} else {
report = await callIocReference(parseIocReference(`${checker}.report`), [])
}
} catch (error) {
report = {
health: { healthy: false, message: error.message },
meta: { fatal: true },
}
}

reportSheet[service] = report
return report.health.healthy
}

/**
* A boolean to know, if all health checks have passed
* or not.
*/
public async isLive (): Promise<boolean> {
if (!this.isReady()) {
return false
}

const { healthy } = await this.report()
return healthy
}

/**
* Add a custom checker to check a given service connectivity
* with the server
*/
public addChecker (service: string, checker: Checker) {
this._healthCheckers[service] = checker
}

/**
* Ensure that application is ready and is not shutting
* down. This relies on the application module.
*/
public isReady () {
return this._application.isReady && !this._application.isShuttingDown
}

/**
* Returns the health check reports. The health checks are performed when
* this method is invoked.
*/
public async report (): Promise<{ healthy: boolean, report: HealthReport }> {
const report: HealthReport = {}
await Promise.all(Object.keys(this._healthCheckers).map((service) => {
return this._invokeChecker(service, report)
}))

/**
* Finding unhealthy service to know if system is healthy or not
*/
const unhealthyService = Object.keys(report).find((service) => !report[service].health.healthy)
return { healthy: !unhealthyService, report }
}
}
156 changes: 156 additions & 0 deletions test/health-check.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
/*
* @adonisjs/core
*
* (c) Harminder Virk <virk@adonisjs.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

/// <reference path="../adonis-typings/application.ts" />
/// <reference path="../adonis-typings/health-check.ts" />

import * as test from 'japa'
import { Ioc } from '@adonisjs/fold'
import { Application } from '@poppinss/application'
import { HealthCheck } from '../src/HealthCheck'

test.group('HealthCheck', () => {
test('use application isReady state to find if application is ready', (assert) => {
const application = new Application(__dirname, new Ioc(), {}, {})
const healthCheck = new HealthCheck(application)

assert.isFalse(healthCheck.isReady())

application.isReady = true
assert.isTrue(healthCheck.isReady())

application.isShuttingDown = true
assert.isFalse(healthCheck.isReady())
})

test('get health checks report', async (assert) => {
const application = new Application(__dirname, new Ioc(), {}, {})
const healthCheck = new HealthCheck(application)

healthCheck.addChecker('event-loop', async () => {
return {
health: {
healthy: true,
},
}
})

const report = await healthCheck.report()
assert.deepEqual(report, {
healthy: true,
report: {
'event-loop': {
health: {
healthy: true,
},
},
},
})
})

test('handle exceptions raised within the checker', async (assert) => {
const application = new Application(__dirname, new Ioc(), {}, {})
const healthCheck = new HealthCheck(application)

healthCheck.addChecker('event-loop', async () => {
throw new Error('boom')
})

const report = await healthCheck.report()
assert.deepEqual(report, {
healthy: false,
report: {
'event-loop': {
health: {
healthy: false,
message: 'boom',
},
meta: {
fatal: true,
},
},
},
})
})

test('set healthy to false when any of the checker fails', async (assert) => {
const application = new Application(__dirname, new Ioc(), {}, {})
const healthCheck = new HealthCheck(application)

healthCheck.addChecker('database', async () => {
return {
health: {
healthy: true,
},
}
})

healthCheck.addChecker('event-loop', async () => {
throw new Error('boom')
})

const report = await healthCheck.report()
assert.deepEqual(report, {
healthy: false,
report: {
'event-loop': {
health: {
healthy: false,
message: 'boom',
},
meta: {
fatal: true,
},
},
'database': {
health: {
healthy: true,
},
},
},
})
})

test('define checker as IoC container binding', async (assert) => {
const ioc = new Ioc()
const application = new Application(__dirname, ioc, {}, {})
const healthCheck = new HealthCheck(application)

class DbChecker {
public async report () {
return {
health: {
healthy: true,
},
}
}
}

ioc.bind('App/Checkers/Db', () => {
return new DbChecker()
})

global[Symbol.for('ioc.make')] = ioc.make.bind(ioc)
global[Symbol.for('ioc.call')] = ioc.call.bind(ioc)

healthCheck.addChecker('database', 'App/Checkers/Db')

const report = await healthCheck.report()
assert.deepEqual(report, {
healthy: true,
report: {
'database': {
health: {
healthy: true,
},
},
},
})
})
})

0 comments on commit df92dee

Please sign in to comment.