├── .editorconfig ├── .github ├── dependabot.yml └── workflows │ └── ci.yml ├── .gitignore ├── LICENSE ├── README.md ├── example └── server.js ├── lib ├── index.d.ts ├── index.js ├── src │ ├── __mocks__ │ │ └── requestController.js │ ├── customHealthCheck.d.ts │ ├── customHealthCheck.js │ ├── health.schema.js │ ├── requestController.js │ └── stats.js └── tests │ ├── customHealthCheck.test.js │ ├── requestController.test.js │ ├── stats.test.js │ └── zod.test.js └── package.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | max_line_length = 80 11 | quote_type=single 12 | 13 | [*.md] 14 | indent_size = 4 15 | trim_trailing_whitespace = false -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: '/' 5 | schedule: 6 | interval: weekly 7 | open-pull-requests-limit: 10 8 | target-branch: master 9 | reviewers: 10 | - gkampitakis 11 | labels: 12 | - dependabot 13 | commit-message: 14 | prefix: fix 15 | prefix-development: chore 16 | include: scope 17 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | paths-ignore: 6 | - "docs/**" 7 | - "*.md" 8 | 9 | jobs: 10 | test: 11 | runs-on: ${{ matrix.os }} 12 | strategy: 13 | matrix: 14 | node-version: [20, 22] 15 | os: [ubuntu-latest] 16 | steps: 17 | - uses: actions/checkout@v4 18 | - name: Use Node.js 19 | uses: actions/setup-node@v4 20 | with: 21 | node-version: ${{ matrix.node-version }} 22 | - name: Install Dependencies 23 | run: | 24 | npm install 25 | - name: Run Tests 26 | run: | 27 | npm test 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .vscode 3 | package-lock.json 4 | coverage 5 | *.zip 6 | /.idea/ 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Georgios Kampitakis 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Fastify Custom Healthcheck 2 | 3 | [![js-semistandard-style](https://img.shields.io/badge/code%20style-semistandard-brightgreen.svg)](https://github.com/standard/semistandard) 4 | [![Build Status](https://travis-ci.com/gkampitakis/fastify-custom-healthcheck.svg?branch=master)](https://travis-ci.com/gkampitakis/fastify-custom-healthcheck) 5 | 6 | `fastify-custom-healthcheck` is a plugin for creating a health route with custom evaluations. 7 | 8 | 9 | ## Install 10 | 11 | ```bash 12 | npm i fastify-custom-healthcheck 13 | ``` 14 | 15 | ## Usage 16 | 17 | Require the module and just register it as any other fastify plugin. From `fastify-custom-healthcheck` on decorator is going to be added to your server for adding custom health checks. 18 | 19 | ```javascript 20 | const fastify = require('fastify')(); 21 | const customHealthCheck = require('fastify-custom-healthCheck'); 22 | 23 | fastify.register(customHealthCheck, options).then(() => { 24 | fastify.addHealthCheck('metric', () => { 25 | return new Promise((resolve) => setTimeout(resolve, 5000)); 26 | }); 27 | }); 28 | 29 | fastify.listen(3000); 30 | ``` 31 | 32 | ## API 33 | 34 | ### register plugin 35 | 36 | ```javascript 37 | fastify.register(customHealthCheck, { 38 | path: '/health/check', // default health 39 | info: {}, // custom information object 40 | }); 41 | ``` 42 | 43 | ### register options 44 | 45 | - `path`: path where you can reach health check route. 46 | - default value: `'/health'`. 47 | - `info`: object where you can define custom information you would like to include in healthcheck response object. 48 | - exposeFailure: Flag that enables additional information to be presented in health check object when a check fails. 49 | - default value: `false` 50 | - schema: If set to true, default schema is used for the route definition, if to false - no schema. If object is passed, it will be used as a schema. This can be used to enable support for custom type providers, e. g. zod or typebox. 51 | - default value: `true` 52 | 53 | ### Decorator 54 | 55 | After registering plugin you can use the decorator for adding custom health checks. 56 | 57 | ```javascript 58 | fastify.addHealthCheck(label, () => {}, { value: true }); 59 | ``` 60 | 61 | ### decorator options 62 | 63 | - value: If you add on `addHealthCheck` a value, when computing health check an equality check happens between evaluation and the value returned by the health check function. If the values are different health check fails. 64 | 65 | ## Example response 66 | 67 | ```json 68 | { 69 | "healthChecks": { 70 | "mongo": { 71 | "status": "FAIL", 72 | "reason": "MongoNetworkError: failed to connect to server [localhost:27017] on first connect [Error: connect ECONNREFUSED 127.0.0.1:27017\n at TCPConnectWrap.afterConnect [as oncomplete] (net.js:1144:16) {\n name: 'MongoNetworkError'\n}]" 73 | }, 74 | "kafka": "HEALTHY", 75 | "redis": "HEALTHY" 76 | }, 77 | "stats": { 78 | "creationTime": "2020-08-04T19:16:29.766Z", 79 | "uptime": 0.303361107, 80 | "memory": { 81 | "rss": 50102272, 82 | "heapTotal": 29270016, 83 | "heapUsed": 16499104, 84 | "external": 20754444, 85 | "arrayBuffers": 19273278 86 | } 87 | }, 88 | "info": { 89 | "example": "Response", 90 | } 91 | } 92 | ``` 93 | 94 | ## Acknowledgements 95 | 96 | This module is inspired by [server-health](https://www.npmjs.com/package/server-health) and the need of having this functionality in fastify. 97 | 98 | ### Example 99 | 100 | You can also check an [example](./example) usage. 101 | 102 | ### Issues 103 | 104 | For any [issues](https://github.com/gkampitakis/fastify-custom-healthcheck/issues). 105 | 106 | ## License 107 | 108 | MIT License 109 | -------------------------------------------------------------------------------- /example/server.js: -------------------------------------------------------------------------------- 1 | const fastify = require('fastify')(); 2 | const MongoClient = require('mongodb').MongoClient; 3 | const customHealthCheck = require('../lib'); 4 | const url = 'mongodb://localhost:27017'; 5 | const _package = require('../package.json'); 6 | 7 | fastify.register(customHealthCheck, { 8 | path: '/custom/path/health', 9 | info: { 10 | example: 'Custom Info', 11 | env: process.env.NODE_ENV, 12 | name: _package.name, 13 | version: _package.version 14 | }, 15 | exposeFailure: true 16 | }) 17 | .then(() => { 18 | fastify.addHealthCheck('random', () => { 19 | return new Promise((resolve, reject) => { 20 | setTimeout(() => { 21 | Math.random() * 100 > 50 ? resolve() : reject(new Error('Random Error')); 22 | }, 5000); 23 | }); 24 | }); 25 | 26 | fastify.addHealthCheck('async', () => { 27 | return new Promise((resolve) => setTimeout(resolve, 2000)); 28 | }); 29 | 30 | fastify.addHealthCheck('mongo', async () => { 31 | const client = await MongoClient.connect(url); 32 | client.db('example'); 33 | 34 | client.close(); 35 | }); 36 | 37 | fastify.addHealthCheck('sync', () => true); 38 | 39 | fastify.addHealthCheck('evaluationCheck', () => { 40 | return new Promise((resolve) => { 41 | setTimeout(() => resolve(true), 2000); 42 | }); 43 | }, { value: false }); 44 | }); 45 | 46 | fastify.listen({ port: 5000 }).then(() => { 47 | console.log('🚀 Example Server listening on port 5000'); 48 | }); 49 | -------------------------------------------------------------------------------- /lib/index.d.ts: -------------------------------------------------------------------------------- 1 | import { FastifyPluginCallback } from 'fastify'; 2 | import { addHealthCheck } from './src/customHealthCheck'; 3 | 4 | export interface CustomHealthCheckOptions { 5 | /** Path where health route is registered */ 6 | path?: string; 7 | /** Object where you can define custom information you would like to include in healthcheck response */ 8 | info?: Record; 9 | /** Flag that enables additional information to be presented in health check object when a check fails. */ 10 | exposeFailure?: boolean; 11 | /** If set to true, default schema is used for the route definition, if to false - no schema. If object is passed, it will be used as a schema. Default value is "true" */ 12 | schema?: boolean | Record; 13 | } 14 | 15 | declare module 'fastify' { 16 | interface FastifyInstance { 17 | addHealthCheck: addHealthCheck; 18 | } 19 | } 20 | 21 | declare const customHealthCheck: FastifyPluginCallback; 22 | export default customHealthCheck; 23 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const fp = require('fastify-plugin'); 3 | const { customHealthCheck } = require('./src/customHealthCheck'); 4 | 5 | module.exports = fp(customHealthCheck); 6 | -------------------------------------------------------------------------------- /lib/src/__mocks__/requestController.js: -------------------------------------------------------------------------------- 1 | function Controller (...args) { 2 | ControllerSpy(...args); 3 | } 4 | 5 | const ControllerSpy = jest.fn(); 6 | 7 | module.exports = Controller; 8 | module.exports.ControllerSpy = ControllerSpy; 9 | -------------------------------------------------------------------------------- /lib/src/customHealthCheck.d.ts: -------------------------------------------------------------------------------- 1 | export interface addHealthCheck { 2 | /** Decorator for adding custom health checks functions 3 | * @param label string for registering checks, must be unique 4 | * @param fn callback function supports Promises 5 | * @param evaluation object containing a value to compare with healthcheck fn return value 6 | */ 7 | ( 8 | label: string, 9 | fn: (...args: any) => Promise | unknown, 10 | evaluation?: evaluation 11 | ): void; 12 | } 13 | 14 | interface evaluation { 15 | /** object or value to compare with healthcheck fn */ 16 | value: unknown; 17 | } 18 | -------------------------------------------------------------------------------- /lib/src/customHealthCheck.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const getStats = require('./stats'); 4 | const Controller = require('./requestController'); 5 | const schema = require('./health.schema'); 6 | 7 | function resolveSchema (options) { 8 | if (options.schema === false) { 9 | return undefined; 10 | } 11 | 12 | if (options.schema === true || options.schema === undefined || options.schema === null) { 13 | return schema; 14 | } 15 | 16 | return options.schema; 17 | } 18 | 19 | function customHealthCheck (fastify, options, next) { 20 | const { path = '/health', info = undefined, exposeFailure = false, ...rest } = options; 21 | const healthChecks = []; 22 | 23 | fastify.decorate('addHealthCheck', function (label, fn, evaluation = null) { 24 | const labelExists = healthChecks.findIndex((h) => (h.label === label)) !== -1; 25 | 26 | if (labelExists) throw Error(`Health check "${label}" already exists`); 27 | 28 | healthChecks.push({ label, fn, evaluation }); 29 | }); 30 | 31 | const resolvedSchema = resolveSchema(options); 32 | fastify.get(path, { ...rest, schema: resolvedSchema }, (req, res) => 33 | Controller(req, res, { healthChecks, info, stats: getStats(), exposeFailure }) 34 | ); 35 | 36 | next(); 37 | } 38 | 39 | module.exports = { 40 | customHealthCheck 41 | }; 42 | -------------------------------------------------------------------------------- /lib/src/health.schema.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const response = { 4 | type: 'object', 5 | properties: { 6 | healthChecks: { 7 | type: 'object', 8 | additionalProperties: true 9 | }, 10 | stats: { 11 | type: 'object', 12 | properties: { 13 | creationTime: { type: 'string', format: 'date-time' }, 14 | uptime: { type: 'number' }, 15 | memory: { 16 | type: 'object', 17 | properties: { 18 | rss: { type: 'number' }, 19 | heapTotal: { type: 'number' }, 20 | heapUsed: { type: 'number' }, 21 | external: { type: 'number' }, 22 | arrayBuffers: { type: 'number' } 23 | } 24 | } 25 | } 26 | }, 27 | info: { 28 | type: 'object', 29 | additionalProperties: true 30 | } 31 | }, 32 | required: ['stats'] 33 | }; 34 | 35 | const healthSchema = { 36 | description: 'Fastify plugin detecting every check passes', 37 | produces: ['application/json'], 38 | tags: ['fastify-custom-healthcheck'], 39 | summary: 'Health check route', 40 | response: { 41 | 200: { 42 | description: 'All health checks passed', 43 | ...response 44 | }, 45 | 500: { 46 | description: 'Health checks failed', 47 | ...response 48 | } 49 | } 50 | }; 51 | 52 | module.exports = healthSchema; 53 | -------------------------------------------------------------------------------- /lib/src/requestController.js: -------------------------------------------------------------------------------- 1 | const equal = require('fast-deep-equal'); 2 | 3 | async function Controller (request, reply, payload) { 4 | const { stats, info, exposeFailure } = payload; 5 | const { healthChecks, status } = await computeHealthChecks( 6 | payload.healthChecks, 7 | exposeFailure 8 | ); 9 | 10 | reply.status(status).send({ 11 | ...(Object.keys(healthChecks).length) && { healthChecks }, 12 | stats, 13 | ...(info && { info }) 14 | }); 15 | } 16 | 17 | async function computeHealthChecks (checks, exposeFailure) { 18 | const healthChecks = {}; 19 | let status = 200; 20 | 21 | const promises = Object.values(checks).map(({ fn }) => fn()); 22 | 23 | const results = await Promise.allSettled(promises); 24 | 25 | checks.forEach(({ label, evaluation }, index) => { 26 | if (results[index].status === 'rejected') { 27 | healthChecks[label] = exposeFailure ? { status: 'FAIL', reason: `${results[index].reason}` } : 'FAIL'; 28 | status = 500; 29 | return; 30 | } 31 | 32 | if (evaluation && !equal(evaluation.value, results[index].value)) { 33 | healthChecks[label] = exposeFailure ? { status: 'FAIL', reason: `Evaluation "${JSON.stringify(evaluation.value)}" failed` } : 'FAIL'; 34 | status = 500; 35 | return; 36 | } 37 | healthChecks[label] = 'HEALTHY'; 38 | }); 39 | 40 | return { healthChecks, status }; 41 | } 42 | 43 | module.exports = Controller; 44 | -------------------------------------------------------------------------------- /lib/src/stats.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const creationTime = new Date().toISOString(); 4 | 5 | const getStats = () => ({ 6 | creationTime, 7 | uptime: process.uptime(), 8 | memory: { ...process.memoryUsage() } 9 | }); 10 | 11 | module.exports = getStats; 12 | -------------------------------------------------------------------------------- /lib/tests/customHealthCheck.test.js: -------------------------------------------------------------------------------- 1 | const { customHealthCheck } = require('../src/customHealthCheck'); 2 | const schema = require('../src/health.schema'); 3 | const getSpy = jest.fn(); 4 | const decorateSpy = jest.fn(); 5 | const fastify = { 6 | get: (...args) => { 7 | getSpy(...args); 8 | args[2]({}, {}); 9 | }, 10 | decorate: decorateSpy 11 | }; 12 | 13 | jest.mock('../src/stats', () => () => ({ 14 | mock: 'mockStats' 15 | })); 16 | jest.mock('../src/requestController'); 17 | 18 | describe('Custom Health Check', () => { 19 | const ControllerMock = jest.requireMock('../src/requestController'); 20 | 21 | beforeEach(() => { 22 | getSpy.mockClear(); 23 | decorateSpy.mockClear(); 24 | ControllerMock.ControllerSpy.mockClear(); 25 | }); 26 | 27 | it('Should create create a decorator', (done) => { 28 | customHealthCheck(fastify, {}, () => { 29 | expect(decorateSpy).toHaveBeenCalledWith('addHealthCheck', expect.any(Function)); 30 | 31 | done(); 32 | }); 33 | }); 34 | 35 | describe('When registering path', () => { 36 | it('Should add a default path', (done) => { 37 | customHealthCheck(fastify, { mock: 'option' }, () => { 38 | expect(getSpy).toHaveBeenCalledWith('/health', { schema, mock: 'option' }, expect.any(Function)); 39 | 40 | done(); 41 | }); 42 | }); 43 | 44 | it('Should add path provided', (done) => { 45 | customHealthCheck(fastify, { mock: 'option', path: '/mock/path' }, () => { 46 | expect(getSpy).toHaveBeenCalledWith('/mock/path', { schema, mock: 'option' }, expect.any(Function)); 47 | 48 | done(); 49 | }); 50 | }); 51 | 52 | it('Should set controller to get path', (done) => { 53 | customHealthCheck(fastify, { mock: 'option', info: 'mockInfo' }, () => { 54 | expect(getSpy).toHaveBeenCalledWith('/health', { schema, mock: 'option' }, expect.any(Function)); 55 | expect(ControllerMock.ControllerSpy).toHaveBeenCalledWith({}, {}, { 56 | healthChecks: [], 57 | info: 'mockInfo', 58 | stats: { 59 | mock: 'mockStats' 60 | }, 61 | exposeFailure: false 62 | }); 63 | 64 | done(); 65 | }); 66 | }); 67 | }); 68 | 69 | describe('When adding healthcheck', () => { 70 | it('Should throw error if try to add the same label twice', () => { 71 | const fastify = { 72 | get: getSpy, 73 | decorate: (...args) => { 74 | decorateSpy(...args); 75 | args[1]('mockLabel'); 76 | args[1]('mockLabel'); 77 | } 78 | }; 79 | 80 | expect(() => customHealthCheck(fastify, { mock: 'option', info: 'mockInfo' }, jest.fn())) 81 | .toThrowError('Health check "mockLabel" already exists'); 82 | }); 83 | }); 84 | }); 85 | -------------------------------------------------------------------------------- /lib/tests/requestController.test.js: -------------------------------------------------------------------------------- 1 | const requestController = require('../src/requestController'); 2 | 3 | describe('Request Controller', () => { 4 | const request = null; 5 | const sendSpy = jest.fn(); 6 | const reply = { 7 | status: jest.fn(() => ({ 8 | send: sendSpy 9 | })) 10 | }; 11 | 12 | beforeEach(() => { 13 | sendSpy.mockClear(); 14 | reply.status.mockClear(); 15 | }); 16 | 17 | it('Should compute health checks and return 200 status', async () => { 18 | const payload = { 19 | info: { mock: 'payload' }, 20 | stats: { mock: 'stats' }, 21 | healthChecks: [{ label: 'test', fn: () => true }, { label: 'test2', fn: () => true }] 22 | }; 23 | 24 | await requestController(request, reply, payload); 25 | expect(reply.status).toHaveBeenCalledWith(200); 26 | expect(sendSpy).toHaveBeenCalledWith({ 27 | info: { mock: 'payload' }, 28 | healthChecks: { 29 | test: 'HEALTHY', 30 | test2: 'HEALTHY' 31 | }, 32 | stats: { mock: 'stats' } 33 | }); 34 | }); 35 | 36 | it('Should compute health checks and return 500 status', async () => { 37 | const payload = { 38 | info: { mock: 'payload' }, 39 | stats: { mock: 'stats' }, 40 | healthChecks: [{ label: 'test', fn: () => Promise.reject(new Error('Error')) }, { label: 'test2', fn: () => true }] 41 | }; 42 | 43 | await requestController(request, reply, payload); 44 | expect(reply.status).toHaveBeenCalledWith(500); 45 | expect(sendSpy).toHaveBeenCalledWith({ 46 | info: { mock: 'payload' }, 47 | healthChecks: { 48 | test: 'FAIL', 49 | test2: 'HEALTHY' 50 | }, 51 | stats: { mock: 'stats' } 52 | }); 53 | }); 54 | 55 | it('Should compute health checks, return 500 status and expose reason of failure', async () => { 56 | const payload = { 57 | stats: {}, 58 | exposeFailure: true, 59 | healthChecks: [{ label: 'test', fn: () => Promise.reject(new Error('Mock Error')) }, { label: 'test2', fn: () => true }] 60 | }; 61 | 62 | await requestController(request, reply, payload); 63 | expect(reply.status).toHaveBeenCalledWith(500); 64 | expect(sendSpy).toHaveBeenCalledWith({ 65 | stats: {}, 66 | healthChecks: { 67 | test: { 68 | status: 'FAIL', 69 | reason: 'Error: Mock Error' 70 | }, 71 | test2: 'HEALTHY' 72 | } 73 | }); 74 | }); 75 | 76 | it('Should compute health check and fail cause of evaluation', async () => { 77 | const payload = { 78 | stats: {}, 79 | exposeFailure: true, 80 | healthChecks: [{ label: 'test', fn: () => false, evaluation: { value: true } }, { label: 'test2', fn: () => true }] 81 | }; 82 | 83 | await requestController(request, reply, payload); 84 | expect(reply.status).toHaveBeenCalledWith(500); 85 | expect(sendSpy).toHaveBeenCalledWith({ 86 | stats: {}, 87 | healthChecks: { 88 | test: { 89 | status: 'FAIL', 90 | reason: 'Evaluation "true" failed' 91 | }, 92 | test2: 'HEALTHY' 93 | } 94 | }); 95 | }); 96 | 97 | it('Should compute health check,fail and hide error', async () => { 98 | const payload = { 99 | stats: {}, 100 | exposeFailure: false, 101 | healthChecks: [{ label: 'test', fn: () => false, evaluation: { value: true } }, { label: 'test2', fn: () => true }] 102 | }; 103 | 104 | await requestController(request, reply, payload); 105 | expect(reply.status).toHaveBeenCalledWith(500); 106 | expect(sendSpy).toHaveBeenCalledWith({ 107 | stats: {}, 108 | healthChecks: { 109 | test: 'FAIL', 110 | test2: 'HEALTHY' 111 | } 112 | }); 113 | }); 114 | 115 | it('Should compute health check and use complex evaluation', async () => { 116 | const payload = { 117 | stats: {}, 118 | exposeFailure: true, 119 | healthChecks: [{ label: 'test', fn: () => ({ some: { nested: { value: 'mock' } } }), evaluation: { value: { some: { nested: { value: 'mock' } } } } }, { label: 'test2', fn: () => true }] 120 | }; 121 | 122 | await requestController(request, reply, payload); 123 | expect(reply.status).toHaveBeenCalledWith(200); 124 | expect(sendSpy).toHaveBeenCalledWith({ 125 | stats: {}, 126 | healthChecks: { 127 | test: 'HEALTHY', 128 | test2: 'HEALTHY' 129 | } 130 | }); 131 | }); 132 | 133 | it('Should not return healthChecks field if no checks are provided', async () => { 134 | const payload = { 135 | stats: {}, 136 | exposeFailure: true, 137 | healthChecks: [] 138 | }; 139 | 140 | await requestController(request, reply, payload); 141 | expect(reply.status).toHaveBeenCalledWith(200); 142 | expect(sendSpy).toHaveBeenCalledWith({ 143 | stats: {} 144 | }); 145 | }); 146 | }); 147 | -------------------------------------------------------------------------------- /lib/tests/stats.test.js: -------------------------------------------------------------------------------- 1 | describe('Stats', () => { 2 | const mockDate = new Date(); 3 | const mockMemoryUsage = { 4 | external: 10, 5 | heapTotal: 10, 6 | heapUsed: 10, 7 | rss: 10 8 | }; 9 | 10 | jest.spyOn(global, 'Date') 11 | .mockImplementation(() => mockDate); 12 | 13 | jest.spyOn(process, 'memoryUsage') 14 | .mockImplementation(() => mockMemoryUsage); 15 | 16 | jest.spyOn(process, 'uptime') 17 | .mockImplementation(() => 10); 18 | it('Should return stats object', () => { 19 | const getStats = require('../src/stats'); 20 | 21 | expect(getStats()).toEqual({ 22 | creationTime: mockDate.toISOString(), 23 | uptime: 10, 24 | memory: { 25 | ...(mockMemoryUsage) 26 | } 27 | }); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /lib/tests/zod.test.js: -------------------------------------------------------------------------------- 1 | const fastify = require('fastify'); 2 | const customHealthCheck = require('../'); 3 | const { 4 | serializerCompiler, 5 | validatorCompiler 6 | } = require('fastify-type-provider-zod'); 7 | const { z } = require('zod'); 8 | 9 | describe('Zod type provider', () => { 10 | let app; 11 | beforeEach(async () => { 12 | app = fastify(); 13 | app.setValidatorCompiler(validatorCompiler); 14 | app.setSerializerCompiler(serializerCompiler); 15 | }); 16 | 17 | afterEach(async () => { 18 | await app.close(); 19 | }); 20 | 21 | it('Works with zod type provider without schema', async () => { 22 | await app.register(customHealthCheck, { 23 | path: '/health', 24 | schema: false 25 | }); 26 | await app.ready(); 27 | 28 | const response = await app.inject().get('/health'); 29 | expect(response.statusCode).toBe(200); 30 | }); 31 | 32 | it('Works with zod type provider with zod schema', async () => { 33 | await app.register(customHealthCheck, { 34 | path: '/health', 35 | schema: { 36 | response: { 37 | 200: z.object({ 38 | healthChecks: z.optional(z.record(z.string())), 39 | stats: z.any(), 40 | info: z.optional(z.record(z.string())) 41 | }) 42 | } 43 | } 44 | }); 45 | await app.ready(); 46 | 47 | const response = await app.inject().get('/health'); 48 | expect(response.statusCode).toBe(200); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fastify-custom-healthcheck", 3 | "version": "4.0.0", 4 | "description": "Fastify plugin that allows to add custom health checks in your server", 5 | "main": "lib/index.js", 6 | "types": "lib/index.d.ts", 7 | "scripts": { 8 | "test": "npm run lint && jest lib/**/*.js", 9 | "clean": "rm -rf /coverage", 10 | "lint": "semistandard | snazzy", 11 | "example": "node example/server.js" 12 | }, 13 | "keywords": [ 14 | "fastify", 15 | "fastify-plugin", 16 | "health", 17 | "healthchecks" 18 | ], 19 | "author": "Georgios Kampitakis", 20 | "license": "MIT", 21 | "files": [ 22 | "lib/**/*", 23 | "!lib/tests/**/*" 24 | ], 25 | "semistandard": { 26 | "env": [ 27 | "jest" 28 | ] 29 | }, 30 | "dependencies": { 31 | "fast-deep-equal": "^3.1.3", 32 | "fastify-plugin": "^5.0.1" 33 | }, 34 | "devDependencies": { 35 | "fastify": "^5.2.0", 36 | "fastify-type-provider-zod": "^4.0.2", 37 | "jest": "^29.7.0", 38 | "mongodb": "^4.7.0", 39 | "semistandard": "^17.0.0", 40 | "snazzy": "^9.0.0", 41 | "zod": "^3.24.1" 42 | }, 43 | "jest": { 44 | "collectCoverage": true, 45 | "verbose": true, 46 | "testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.js$", 47 | "moduleFileExtensions": [ 48 | "js" 49 | ], 50 | "collectCoverageFrom": [ 51 | "lib/src/**/*.js", 52 | "!lib/src/**/*/index.js", 53 | "!lib/src/index.js" 54 | ], 55 | "coverageThreshold": { 56 | "global": { 57 | "branches": 100, 58 | "functions": 100, 59 | "lines": 100, 60 | "statements": 100 61 | } 62 | } 63 | }, 64 | "publishConfig": { 65 | "registry": "https://registry.npmjs.org/" 66 | }, 67 | "bugs": { 68 | "url": "https://github.com/gkampitakis/fastify-custom-healthcheck/issues" 69 | }, 70 | "repository": { 71 | "url": "https://github.com/gkampitakis/fastify-custom-healthcheck" 72 | } 73 | } 74 | --------------------------------------------------------------------------------