Skip to content

Commit

Permalink
MET-425 Create a common healthcheck plugin for public and private end…
Browse files Browse the repository at this point in the history
…points
  • Loading branch information
andrewi-wd committed Nov 25, 2024
1 parent 59cebe0 commit 4fae725
Show file tree
Hide file tree
Showing 3 changed files with 529 additions and 0 deletions.
36 changes: 36 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,42 @@ Add the plugin to your Fastify instance by registering it with the following opt

Your Fastify app will reply with the status of the app when hitting the `GET /` route.

### Common Healthcheck Plugin

Plugin to monitor app status through public and private healthchecks.

Add the plugin to your Fastify instance by registering it with the following options:

- `healthChecks`, a list of promises with healthcheck in the callback;
- `responsePayload` (optional), the response payload that the public healthcheck should return. If no response payload is provided, the default response is:
```json
{ "heartbeat": "HEALTHY", "checks": {} }
```

Your Fastify app will reply with the status of the app when hitting the `GET /` route with aggregated results from healthchecks provided, example:
```json
{
"heartbeat": "HEALTHY",
"checks": {
"aggregation": "HEALTHY"
}
}
```



Your Fastify app will reply with the status of the app when hitting the `GET /health` route with detailed results from healthchecks provided, example:
```json
{
"heartbeat": "PARTIALLY_HEALTHY",
"checks": {
"check1": "HEALTHY",
"check2": "HEALTHY",
"check3": "FAIL"
}
}
```

### Split IO Plugin

Plugin to handle feature flags in Split IO.
Expand Down
319 changes: 319 additions & 0 deletions lib/plugins/healthcheck/commonHealthcheckPlugin.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,319 @@
import type { FastifyInstance } from 'fastify'
import fastify from 'fastify'

import type { HealthChecker } from './healthcheckCommons'
import { commonHealthcheckPlugin, type CommonHealthcheckPluginOptions } from './commonHealthcheckPlugin.js'
import { describe } from 'vitest'

const positiveHealthcheckChecker: HealthChecker = () => {
return Promise.resolve({ result: true })
}
const negativeHealthcheckChecker: HealthChecker = () => {
return Promise.resolve({ error: new Error('Something exploded') })
}

async function initApp(opts: CommonHealthcheckPluginOptions) {
const app = fastify()
await app.register(commonHealthcheckPlugin, opts)
await app.ready()
return app
}

const PUBLIC_ENDPOINT = '/'
const PRIVATE_ENDPOINT = '/health'

describe('commonHealthcheckPlugin', () => {
let app: FastifyInstance
afterAll(async () => {
await app.close()
})

describe('public endpoint', () => {
it('returns a heartbeat', async () => {
app = await initApp({ healthChecks: [] })

const response = await app.inject().get(PUBLIC_ENDPOINT).end()
expect(response.statusCode).toBe(200)
expect(response.json()).toEqual({ heartbeat: 'HEALTHY', checks: {} })
})

it('returns custom heartbeat', async () => {
app = await initApp({ responsePayload: { version: 1 }, healthChecks: [] })

const response = await app.inject().get(PUBLIC_ENDPOINT).end()
expect(response.statusCode).toBe(200)
expect(response.json()).toEqual({
heartbeat: 'HEALTHY',
version: 1,
checks: {},
})
})

it('returns false if one mandatory healthcheck fails', async () => {
app = await initApp({
responsePayload: { version: 1 },
healthChecks: [
{
name: 'check1',
isMandatory: true,
checker: negativeHealthcheckChecker,
},
{
name: 'check2',
isMandatory: true,
checker: positiveHealthcheckChecker,
},
],
})

const response = await app.inject().get(PUBLIC_ENDPOINT).end()
expect(response.statusCode).toBe(500)
expect(response.json()).toEqual({
heartbeat: 'FAIL',
version: 1,
checks: {
aggregation: 'FAIL',
},
})
})

it('returns partial if optional healthcheck fails', async () => {
app = await initApp({
responsePayload: { version: 1 },
healthChecks: [
{
name: 'check1',
isMandatory: false,
checker: negativeHealthcheckChecker,
},
{
name: 'check2',
isMandatory: true,
checker: positiveHealthcheckChecker,
},
],
})

const response = await app.inject().get(PUBLIC_ENDPOINT).end()
expect(response.statusCode).toBe(200)
expect(response.json()).toEqual({
heartbeat: 'PARTIALLY_HEALTHY',
version: 1,
checks: {
aggregation: 'PARTIALLY_HEALTHY',
},
})
})

it('returns true if all healthchecks pass', async () => {
app = await initApp({
responsePayload: { version: 1 },
healthChecks: [
{
name: 'check1',
isMandatory: true,
checker: positiveHealthcheckChecker,
},
{
name: 'check2',
isMandatory: true,
checker: positiveHealthcheckChecker,
},
],
})

const response = await app.inject().get(PUBLIC_ENDPOINT).end()
expect(response.statusCode).toBe(200)
expect(response.json()).toEqual({
heartbeat: 'HEALTHY',
version: 1,
checks: {
aggregation: 'HEALTHY',
},
})
})

it('omits extra info if data provider is set', async () => {
app = await initApp({
responsePayload: { version: 1 },
healthChecks: [
{
name: 'check1',
isMandatory: true,
checker: positiveHealthcheckChecker,
},
],
infoProviders: [
{
name: 'provider1',
dataResolver: () => {
return {
someData: 1,
}
},
},
],
})

const response = await app.inject().get(PUBLIC_ENDPOINT).end()
expect(response.statusCode).toBe(200)
expect(response.json()).toEqual({
heartbeat: 'HEALTHY',
version: 1,
checks: {
aggregation: 'HEALTHY',
},
})
})
})

describe('private endpoint', () => {
it('returns a heartbeat', async () => {
app = await initApp({ healthChecks: [] })

const response = await app.inject().get(PRIVATE_ENDPOINT).end()
expect(response.statusCode).toBe(200)
expect(response.json()).toEqual({ heartbeat: 'HEALTHY', checks: {} })
})

it('returns custom heartbeat', async () => {
app = await initApp({ responsePayload: { version: 1 }, healthChecks: [] })

const response = await app.inject().get(PRIVATE_ENDPOINT).end()
expect(response.statusCode).toBe(200)
expect(response.json()).toEqual({
heartbeat: 'HEALTHY',
version: 1,
checks: {},
})
})

it('returns false if one mandatory healthcheck fails', async () => {
app = await initApp({
responsePayload: { version: 1 },
healthChecks: [
{
name: 'check1',
isMandatory: true,
checker: negativeHealthcheckChecker,
},
{
name: 'check2',
isMandatory: true,
checker: positiveHealthcheckChecker,
},
],
})

const response = await app.inject().get(PRIVATE_ENDPOINT).end()
expect(response.statusCode).toBe(500)
expect(response.json()).toEqual({
heartbeat: 'FAIL',
version: 1,
checks: {
check1: 'FAIL',
check2: 'HEALTHY',
},
})
})

it('returns partial if optional healthcheck fails', async () => {
app = await initApp({
responsePayload: { version: 1 },
healthChecks: [
{
name: 'check1',
isMandatory: false,
checker: negativeHealthcheckChecker,
},
{
name: 'check2',
isMandatory: true,
checker: positiveHealthcheckChecker,
},
],
})

const response = await app.inject().get(PRIVATE_ENDPOINT).end()
expect(response.statusCode).toBe(200)
expect(response.json()).toEqual({
heartbeat: 'PARTIALLY_HEALTHY',
version: 1,
checks: {
check1: 'FAIL',
check2: 'HEALTHY',
},
})
})

it('returns true if all healthchecks pass', async () => {
app = await initApp({
responsePayload: { version: 1 },
healthChecks: [
{
name: 'check1',
isMandatory: true,
checker: positiveHealthcheckChecker,
},
{
name: 'check2',
isMandatory: true,
checker: positiveHealthcheckChecker,
},
],
})

const response = await app.inject().get(PRIVATE_ENDPOINT).end()
expect(response.statusCode).toBe(200)
expect(response.json()).toEqual({
heartbeat: 'HEALTHY',
version: 1,
checks: {
check1: 'HEALTHY',
check2: 'HEALTHY',
},
})
})

it('returns extra info if data provider is set', async () => {
app = await initApp({
responsePayload: { version: 1 },
healthChecks: [
{
name: 'check1',
isMandatory: true,
checker: positiveHealthcheckChecker,
},
],
infoProviders: [
{
name: 'provider1',
dataResolver: () => {
return {
someData: 1,
}
},
},
],
})

const response = await app.inject().get(PRIVATE_ENDPOINT).end()
expect(response.statusCode).toBe(200)
expect(response.json()).toEqual({
heartbeat: 'HEALTHY',
version: 1,
checks: {
check1: 'HEALTHY',
},
extraInfo: [
{
name: 'provider1',
value: {
someData: 1,
},
},
],
})
})
})
})
Loading

0 comments on commit 4fae725

Please sign in to comment.