diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..6fd0892 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*] +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true +indent_style = space +indent_size = 2 \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b512c09 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules \ No newline at end of file diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..8857551 --- /dev/null +++ b/.npmignore @@ -0,0 +1,3 @@ +.editorconfig +.node-version +test diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..9cf9495 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +package-lock=false \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..66fd28f --- /dev/null +++ b/.travis.yml @@ -0,0 +1,6 @@ +language: node_js +node_js: + - "9" + - "8" + +after_script: "npm run coveralls" diff --git a/README.md b/README.md index 75dc5b2..d1c6e17 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,29 @@ # hapi-healthcheck server monitoring + + +### Options: + - `token` - Optional token to secure the endpoint. + - `endpoint` - Route to use. Defaults to `/health` + - `checks` - array objects for each check. + - `name` - Name of the check, used as the key in the response json. + - `method` - server method to call. Will be called as `async method(request, options)`. Function should return json. + - `options` - optional data to pass to the method. + +### Example output: + +```json +{ + "host": "sorrynotsorry.local", + "env": "test", + "uptime": 0, + "cpu": { "user": 384085, "system": 48978 }, + "memory": { + "rss": 47497216, + "heapTotal": 32874496, + "heapUsed": 18066608, + "external": 518509 + }, + "customCheck": { "test": 1 } +} +``` diff --git a/index.js b/index.js new file mode 100644 index 0000000..2de2011 --- /dev/null +++ b/index.js @@ -0,0 +1,54 @@ +const async = require('async'); +const Boom = require('boom'); +const str2fn = require('str2fn'); + +const defaults = { + token: false, + endpoint: '/health', + checks: [] +}; + +const register = function(server, options) { + const settings = Object.assign({}, defaults, options); + + server.route({ + method: 'get', + path: settings.endpoint, + async handler(request, h) { + if (settings.token && settings.token !== request.query.token) { + throw Boom.unauthorized(); + } + + const output = { + host: server.info.host, + env: process.env.NODE_ENV, + uptime: Math.floor(process.uptime()), + cpu: process.cpuUsage(), + memory: process.memoryUsage() + }; + + await async.each(settings.checks, async check => { + if (!check.name || !check.method) { + throw new Boom('Invalid check'); + } + + output[check.name] = await str2fn.execute( + check.method, + server.methods, + { + request, + options: check.options + } + ); + }); + + return output; + } + }); +}; + +exports.plugin = { + once: true, + pkg: require('./package.json'), + register +}; diff --git a/package.json b/package.json new file mode 100644 index 0000000..17d21c7 --- /dev/null +++ b/package.json @@ -0,0 +1,52 @@ +{ + "name": "hapi-healthcheck", + "version": "1.0.0", + "description": "Server monitoring", + "main": "index.js", + "scripts": { + "test": "lab --leaks -e test -c ", + "coveralls": "lab -r lcov | coveralls" + }, + "engine": { + "node": ">=8.0.0" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/firstandthird/hapi-healthcheck.git" + }, + "keywords": [ + "hapi", + "healthcheck", + "monitoring" + ], + "eslintConfig": { + "extends": "firstandthird", + "rules": { + "no-underscore-dangle": 0 + }, + "parserOptions": { + "ecmaVersion": 2017 + } + }, + "author": "First+Third", + "license": "MIT", + "bugs": { + "url": "https://github.com/firstandthird/hapi-healthcheck/issues" + }, + "homepage": "https://github.com/firstandthird/hapi-healthcheck#readme", + "dependencies": { + "async": "^2.6.0", + "boom": "^7.1.1", + "str2fn": "^3.0.0" + }, + "devDependencies": { + "code": "^5.1.2", + "coveralls": "^3.0.0", + "eslint": "^4.11.0", + "eslint-config-firstandthird": "^4.0.1", + "eslint-plugin-import": "^2.6.1", + "hapi": "^17.2.0", + "lab": "^15.1.2", + "nodemon": "^1.14.7" + } +} diff --git a/test/healthcheck.test.js b/test/healthcheck.test.js new file mode 100644 index 0000000..e9180d6 --- /dev/null +++ b/test/healthcheck.test.js @@ -0,0 +1,158 @@ +const hapi = require('hapi'); +const Lab = require('lab'); +const lab = exports.lab = Lab.script(); +const code = require('code'); +const wreck = require('wreck'); +const hapiHealthcheck = require('../index.js'); +let server; + +lab.beforeEach(() => { + server = new hapi.Server({ port: 8000 }); +}); + +lab.afterEach(async () => { + await server.stop(); +}); + +lab.test('default options', async() => { + await server.register(hapiHealthcheck); + await server.start(); + + const result = await wreck.get('http://localhost:8000/health', { json: 'force' }); + + code.expect(result.payload).to.be.an.object(); + code.expect(result.payload.host).to.equal(server.info.host); + code.expect(result.payload.env).to.equal(process.env.NODE_ENV); + code.expect(result.payload.uptime).to.exist(); + code.expect(result.payload.cpu).to.be.an.object(); + code.expect(result.payload.memory).to.be.an.object(); +}); + +lab.test('token enabled but not passed', async() => { + await server.register({ + plugin: hapiHealthcheck, + options: { + token: '1234' + } + }); + await server.start(); + + try { + await wreck.get('http://localhost:8000/health', { json: 'force' }); + } catch (e) { + code.expect(e.output.statusCode).to.equal(401); + } +}); + +lab.test('token passed', async() => { + await server.register({ + plugin: hapiHealthcheck, + options: { + token: '1234' + } + }); + await server.start(); + + const result = await wreck.get('http://localhost:8000/health?token=1234', { json: 'force' }); + + code.expect(result.payload).to.be.an.object(); + code.expect(result.payload.host).to.equal(server.info.host); + code.expect(result.payload.env).to.equal(process.env.NODE_ENV); + code.expect(result.payload.uptime).to.exist(); + code.expect(result.payload.cpu).to.be.an.object(); + code.expect(result.payload.memory).to.be.an.object(); +}); + + +lab.test('custom checks', async() => { + server.method('get.test', () => ({ + test: 1 + })); + + server.method('get.test2', (request, options) => ({ + test: options.testNumber + })); + + await server.register({ + plugin: hapiHealthcheck, + options: { + checks: [ + { + name: 'test1', + method: 'get.test' + }, + { + name: 'test2', + method: 'get.test2(request, options)', + options: { + testNumber: 2 + } + } + ] + } + }); + await server.start(); + + const result = await wreck.get('http://localhost:8000/health', { json: 'force' }); + + code.expect(result.payload).to.be.an.object(); + code.expect(result.payload.host).to.equal(server.info.host); + code.expect(result.payload.env).to.equal(process.env.NODE_ENV); + code.expect(result.payload.uptime).to.exist(); + code.expect(result.payload.cpu).to.be.an.object(); + code.expect(result.payload.memory).to.be.an.object(); + code.expect(result.payload.test1).to.be.an.object(); + code.expect(result.payload.test1.test).to.equal(1); + code.expect(result.payload.test2).to.be.an.object(); + code.expect(result.payload.test2.test).to.equal(2); +}); + +lab.test('check missing name', async() => { + server.method('get.test', () => ({ + test: 1 + })); + + await server.register({ + plugin: hapiHealthcheck, + options: { + checks: [ + { + method: 'get.test' + } + ] + } + }); + await server.start(); + + try { + await wreck.get('http://localhost:8000/health', { json: 'force' }); + code.fail('something wrong'); + } catch (e) { + code.expect(e).to.exist(); + } +}); + +lab.test('check missing method', async() => { + server.method('get.test', () => ({ + test: 1 + })); + + await server.register({ + plugin: hapiHealthcheck, + options: { + checks: [ + { + name: 'test' + } + ] + } + }); + await server.start(); + + try { + await wreck.get('http://localhost:8000/health', { json: 'force' }); + code.fail('something wrong'); + } catch (e) { + code.expect(e).to.exist(); + } +});