├── .gitignore ├── .npmignore ├── .prettierignore ├── .prettierrc ├── .travis.yml ├── LICENSE ├── README.md ├── examples └── test-status-monitor │ ├── .prettierrc │ ├── README.md │ ├── nest-cli.json │ ├── nodemon-debug.json │ ├── nodemon.json │ ├── package-lock.json │ ├── package.json │ ├── src │ ├── app.controller.spec.ts │ ├── app.controller.ts │ ├── app.module.ts │ ├── app.service.ts │ ├── healthController.ts │ ├── main.hmr.ts │ └── main.ts │ ├── test │ ├── app.e2e-spec.ts │ └── jest-e2e.json │ ├── tsconfig.json │ ├── tsconfig.spec.json │ ├── tslint.json │ └── webpack.config.js ├── package-lock.json ├── package.json ├── src ├── config │ ├── chart.visibility.configuration.ts │ ├── health.check.configuration.ts │ ├── spans.configuration.ts │ └── status.monitor.configuration.ts ├── default.config.ts ├── health.check.service.ts ├── index.ts ├── public │ ├── index.html │ ├── javascripts │ │ └── app.js │ └── stylesheets │ │ └── style.css ├── status.monitor.constants.ts ├── status.monitor.controller.ts ├── status.monitor.gateway.ts ├── status.monitor.middleware.ts ├── status.monitor.module.ts └── status.monitoring.service.ts ├── tests └── setup.spec.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | coverage -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .idea 2 | examples 3 | node_modules -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | src/public/** -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "printWidth": 80, 4 | "proseWrap": "always", 5 | "tabWidth": 2, 6 | "useTabs": false, 7 | "trailingComma": "all", 8 | "bracketSpacing": true, 9 | "jsxBracketSameLine": false, 10 | "semi": true 11 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | cache: 4 | directories: 5 | - node_modules 6 | notifications: 7 | email: true 8 | node_js: 9 | - 10 10 | - 9 11 | - 8 12 | before_install: 13 | - npm install -g npm@latest 14 | before_script: 15 | - npm prune 16 | script: 17 | - npm run test 18 | after_success: 19 | - npm run coveralls -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 GenFirst 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nest-status-monitor 2 | 3 | [![NPM](https://nodei.co/npm/nest-status-monitor.png?downloads=true&downloadRank=true&stars=true)](https://nodei.co/npm/nest-status-monitor/) 4 | 5 | [![nest-status-monitor on npm](https://img.shields.io/npm/v/nest-status-monitor.svg)](https://www.npmjs.com/package/nest-status-monitor) 6 | [![npm](https://img.shields.io/npm/dt/nest-status-monitor.svg)](https://img.shields.io/npm/dt/nest-status-monitor.svg) 7 | [![Build Status](https://travis-ci.org/GenFirst/nest-status-monitor.svg?branch=master)](https://travis-ci.org/GenFirst/nest-status-monitor) 8 | [![Coverage Status](https://coveralls.io/repos/github/GenFirst/nest-status-monitor/badge.svg?branch=master)](https://coveralls.io/github/GenFirst/nest-status-monitor?branch=master) 9 | ![License](https://img.shields.io/badge/license-MIT-blue.svg) 10 | [![Edit nest-status-monitor](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/yw1lov19q9?autoresize=1&initialpath=%2Fstatus) 11 | 12 | Simple, self-hosted module based on Socket.io and Chart.js to report realtime 13 | server metrics for Nest.js based node servers. 14 | 15 | ![Status monitor page](https://i.imgur.com/AkZEPYG.gif 'Status monitor page') 16 | 17 | ## Demo 18 | 19 | Demo can be found [here](https://nest-status-monitor.herokuapp.com/status) 20 | 21 | ## Installation & setup Nest.js v6 22 | 23 | 1. Run `npm install nest-status-monitor --save` 24 | 2. Setup module: 25 | 26 | ```javascript 27 | @Module({ 28 | imports: [StatusMonitorModule.setUp(statusMonitorConfig)], 29 | ``` 30 | 31 | 3. Run server and go to `/status` 32 | 33 | ## Installation & setup Nest.js v5 34 | 35 | 1. Run `npm install nest-status-monitor@0.0.3 --save` 36 | 2. Setup module: 37 | 38 | ```javascript 39 | @Module({ 40 | imports: [StatusMonitorModule.setUp(statusMonitorConfig)], 41 | ``` 42 | 43 | 3. Run server and go to `/status` 44 | 45 | ## Run examples 46 | 47 | 1. Go to `cd examples/test-status-monitor` 48 | 2. Run `npm i` 49 | 3. Run server `npm start` 50 | 4. Go to `http://localhost:3001` 51 | 52 | ## Options 53 | 54 | Monitor can be configured by passing options object during initialization of 55 | module. 56 | 57 | Default config: 58 | 59 | ```javascript 60 | pageTitle: 'Nest.js Monitoring Page', 61 | port: 3001, 62 | path: '/status', 63 | ignoreStartsWith: '/health/alive', 64 | spans: [ 65 | { 66 | interval: 1, // Every second 67 | retention: 60, // Keep 60 datapoints in memory 68 | }, 69 | { 70 | interval: 5, // Every 5 seconds 71 | retention: 60, 72 | }, 73 | { 74 | interval: 15, // Every 15 seconds 75 | retention: 60, 76 | } 77 | ], 78 | chartVisibility: { 79 | cpu: true, 80 | mem: true, 81 | load: true, 82 | responseTime: true, 83 | rps: true, 84 | statusCodes: true, 85 | }, 86 | healthChecks: [] 87 | ``` 88 | 89 | ## Health Checks 90 | 91 | You can add a series of health checks to the configuration that will appear 92 | below the other stats. The health check will be considered successful if the 93 | endpoint returns a 200 status code. 94 | 95 | ```javascript 96 | // config 97 | healthChecks: [ 98 | { 99 | protocol: 'http', 100 | host: 'localhost', 101 | path: '/health/alive', 102 | port: 3001, 103 | }, 104 | { 105 | protocol: 'http', 106 | host: 'localhost', 107 | path: '/health/dead', 108 | port: 3001, 109 | }, 110 | ]; 111 | ``` 112 | 113 | ## License 114 | 115 | [MIT License](https://opensource.org/licenses/MIT) © Ivan Vasiljevic 116 | 117 | Forked from 118 | [express-status-monitor](https://github.com/RafalWilinski/express-status-monitor) 119 | -------------------------------------------------------------------------------- /examples/test-status-monitor/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } -------------------------------------------------------------------------------- /examples/test-status-monitor/README.md: -------------------------------------------------------------------------------- 1 |

2 | Nest Logo 3 |

4 | 5 | [travis-image]: https://api.travis-ci.org/nestjs/nest.svg?branch=master 6 | [travis-url]: https://travis-ci.org/nestjs/nest 7 | [linux-image]: https://img.shields.io/travis/nestjs/nest/master.svg?label=linux 8 | [linux-url]: https://travis-ci.org/nestjs/nest 9 | 10 |

A progressive Node.js framework for building efficient and scalable server-side applications, heavily inspired by Angular.

11 |

12 | NPM Version 13 | Package License 14 | NPM Downloads 15 | Travis 16 | Linux 17 | Coverage 18 | Gitter 19 | Backers on Open Collective 20 | Sponsors on Open Collective 21 | 22 | 23 |

24 | 26 | 27 | ## Description 28 | 29 | [Nest](https://github.com/nestjs/nest) framework TypeScript starter repository. 30 | 31 | ## Installation 32 | 33 | ```bash 34 | $ npm install 35 | ``` 36 | 37 | ## Running the app 38 | 39 | ```bash 40 | # development 41 | $ npm run start 42 | 43 | # watch mode 44 | $ npm run start:dev 45 | 46 | # incremental rebuild (webpack) 47 | $ npm run webpack 48 | $ npm run start:hmr 49 | 50 | # production mode 51 | $ npm run start:prod 52 | ``` 53 | 54 | ## Test 55 | 56 | ```bash 57 | # unit tests 58 | $ npm run test 59 | 60 | # e2e tests 61 | $ npm run test:e2e 62 | 63 | # test coverage 64 | $ npm run test:cov 65 | ``` 66 | 67 | ## Support 68 | 69 | Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support). 70 | 71 | ## Stay in touch 72 | 73 | - Author - [Kamil Myśliwiec](https://kamilmysliwiec.com) 74 | - Website - [https://nestjs.com](https://nestjs.com/) 75 | - Twitter - [@nestframework](https://twitter.com/nestframework) 76 | 77 | ## License 78 | 79 | Nest is [MIT licensed](LICENSE). 80 | -------------------------------------------------------------------------------- /examples/test-status-monitor/nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "language": "ts", 3 | "collection": "@nestjs/schematics", 4 | "sourceRoot": "src" 5 | } 6 | -------------------------------------------------------------------------------- /examples/test-status-monitor/nodemon-debug.json: -------------------------------------------------------------------------------- 1 | { 2 | "watch": ["src"], 3 | "ext": "ts", 4 | "ignore": ["src/**/*.spec.ts"], 5 | "exec": "node --inspect-brk -r ts-node/register src/main.ts" 6 | } -------------------------------------------------------------------------------- /examples/test-status-monitor/nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "watch": ["src"], 3 | "ext": "ts", 4 | "ignore": ["src/**/*.spec.ts"], 5 | "exec": "ts-node -r tsconfig-paths/register src/main.ts" 6 | } 7 | -------------------------------------------------------------------------------- /examples/test-status-monitor/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-status-monitor", 3 | "version": "1.0.1", 4 | "description": "Example how to use status monitor module", 5 | "author": "Ivan Vasiljevic", 6 | "license": "MIT", 7 | "scripts": { 8 | "format": "prettier --write \"src/**/*.ts\"", 9 | "start": "ts-node -r tsconfig-paths/register src/main.ts", 10 | "start:dev": "nodemon", 11 | "start:debug": "nodemon --config nodemon-debug.json", 12 | "prestart:prod": "rimraf dist && tsc", 13 | "start:prod": "node dist/main.js", 14 | "start:hmr": "node dist/server", 15 | "lint": "tslint -p tsconfig.json -c tslint.json", 16 | "test": "jest", 17 | "test:watch": "jest --watch", 18 | "test:cov": "jest --coverage", 19 | "test:e2e": "jest --config ./test/jest-e2e.json", 20 | "webpack": "webpack --config webpack.config.js" 21 | }, 22 | "dependencies": { 23 | "@nestjs/common": "^6.11.11", 24 | "@nestjs/core": "^6.11.11", 25 | "@nestjs/platform-express": "^6.11.11", 26 | "@nestjs/platform-socket.io": "^6.11.11", 27 | "@nestjs/websockets": "^6.11.11", 28 | "reflect-metadata": "^0.1.12", 29 | "rxjs": "^6.2.2", 30 | "typescript": "^3.0.1" 31 | }, 32 | "devDependencies": { 33 | "@nestjs/testing": "^6.11.11", 34 | "@types/express": "^4.16.0", 35 | "@types/jest": "^23.3.1", 36 | "@types/node": "^10.7.1", 37 | "@types/socket.io": "^2.1.2", 38 | "@types/supertest": "^2.0.5", 39 | "jest": "^23.5.0", 40 | "nodemon": "^1.18.3", 41 | "prettier": "^1.14.2", 42 | "rimraf": "^2.6.2", 43 | "supertest": "^3.1.0", 44 | "ts-jest": "^23.1.3", 45 | "ts-loader": "^4.4.2", 46 | "ts-node": "^7.0.1", 47 | "tsconfig-paths": "^3.5.0", 48 | "tslint": "5.11.0", 49 | "webpack": "^4.16.5", 50 | "webpack-cli": "^3.1.0", 51 | "webpack-node-externals": "^1.7.2" 52 | }, 53 | "jest": { 54 | "moduleFileExtensions": [ 55 | "js", 56 | "json", 57 | "ts" 58 | ], 59 | "rootDir": "src", 60 | "testRegex": ".spec.ts$", 61 | "transform": { 62 | "^.+\\.(t|j)s$": "ts-jest" 63 | }, 64 | "coverageDirectory": "../coverage", 65 | "testEnvironment": "node" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /examples/test-status-monitor/src/app.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { INestApplication } from '@nestjs/common'; 3 | import { AppController } from './app.controller'; 4 | import { AppService } from './app.service'; 5 | 6 | describe('AppController', () => { 7 | let app: TestingModule; 8 | 9 | beforeAll(async () => { 10 | app = await Test.createTestingModule({ 11 | controllers: [AppController], 12 | providers: [AppService], 13 | }).compile(); 14 | }); 15 | 16 | describe('root', () => { 17 | it('should return "Hello World!"', () => { 18 | const appController = app.get(AppController); 19 | expect(appController.root()).toBe('Hello World!'); 20 | }); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /examples/test-status-monitor/src/app.controller.ts: -------------------------------------------------------------------------------- 1 | import { Get, Controller } from '@nestjs/common'; 2 | import { AppService } from './app.service'; 3 | 4 | @Controller() 5 | export class AppController { 6 | constructor(private readonly appService: AppService) {} 7 | 8 | @Get() 9 | root(): string { 10 | return this.appService.root(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /examples/test-status-monitor/src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { AppController } from './app.controller'; 3 | import { AppService } from './app.service'; 4 | import { 5 | StatusMonitorModule, 6 | StatusMonitorConfiguration, 7 | } from '../../../dist/index'; 8 | import { HealthController } from './healthController'; 9 | 10 | const statusMonitorConfig: StatusMonitorConfiguration = { 11 | pageTitle: 'Nest.js Monitoring Page', 12 | port: 3001, 13 | path: '/status', 14 | ignoreStartsWith: '/healt/alive', 15 | healthChecks: [ 16 | { 17 | protocol: 'http', 18 | host: 'localhost', 19 | path: '/health/alive', 20 | port: 3001, 21 | }, 22 | { 23 | protocol: 'http', 24 | host: 'localhost', 25 | path: '/health/dead', 26 | port: 3001, 27 | }, 28 | ], 29 | spans: [ 30 | { 31 | interval: 1, // Every second 32 | retention: 60, // Keep 60 datapoints in memory 33 | }, 34 | { 35 | interval: 5, // Every 5 seconds 36 | retention: 60, 37 | }, 38 | { 39 | interval: 15, // Every 15 seconds 40 | retention: 60, 41 | }, 42 | { 43 | interval: 60, // Every 60 seconds 44 | retention: 600, 45 | }, 46 | ], 47 | chartVisibility: { 48 | cpu: true, 49 | mem: true, 50 | load: true, 51 | responseTime: true, 52 | rps: true, 53 | statusCodes: true, 54 | }, 55 | }; 56 | 57 | @Module({ 58 | imports: [StatusMonitorModule.setUp(statusMonitorConfig)], 59 | controllers: [AppController, HealthController], 60 | providers: [AppService], 61 | }) 62 | export class AppModule {} 63 | -------------------------------------------------------------------------------- /examples/test-status-monitor/src/app.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | @Injectable() 4 | export class AppService { 5 | root(): string { 6 | return 'Hello World!'; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /examples/test-status-monitor/src/healthController.ts: -------------------------------------------------------------------------------- 1 | import { Get, Controller, HttpCode } from '@nestjs/common'; 2 | 3 | @Controller('health') 4 | export class HealthController { 5 | @Get('alive') 6 | @HttpCode(200) 7 | alive(): string { 8 | return 'OK'; 9 | } 10 | 11 | @Get('dead') 12 | @HttpCode(500) 13 | dead(): string { 14 | return 'DEAD'; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /examples/test-status-monitor/src/main.hmr.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core'; 2 | import { AppModule } from './app.module'; 3 | 4 | declare const module: any; 5 | 6 | async function bootstrap() { 7 | const app = await NestFactory.create(AppModule); 8 | await app.listen(3001); 9 | 10 | if (module.hot) { 11 | module.hot.accept(); 12 | module.hot.dispose(() => app.close()); 13 | } 14 | } 15 | bootstrap(); 16 | -------------------------------------------------------------------------------- /examples/test-status-monitor/src/main.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core'; 2 | import { AppModule } from './app.module'; 3 | import { NestExpressApplication } from '@nestjs/platform-express'; 4 | 5 | async function bootstrap() { 6 | const app = await NestFactory.create(AppModule); 7 | await app.listen(3001); 8 | } 9 | bootstrap(); 10 | -------------------------------------------------------------------------------- /examples/test-status-monitor/test/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { INestApplication } from '@nestjs/common'; 2 | import { Test } from '@nestjs/testing'; 3 | import * as request from 'supertest'; 4 | import { AppModule } from './../src/app.module'; 5 | 6 | describe('AppController (e2e)', () => { 7 | let app: INestApplication; 8 | 9 | beforeAll(async () => { 10 | const moduleFixture = await Test.createTestingModule({ 11 | imports: [AppModule], 12 | }).compile(); 13 | 14 | app = moduleFixture.createNestApplication(); 15 | await app.init(); 16 | }); 17 | 18 | it('/ (GET)', () => { 19 | return request(app.getHttpServer()) 20 | .get('/') 21 | .expect(200) 22 | .expect('Hello World!'); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /examples/test-status-monitor/test/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": ["js", "json", "ts"], 3 | "rootDir": ".", 4 | "testEnvironment": "node", 5 | "testRegex": ".e2e-spec.ts$", 6 | "transform": { 7 | "^.+\\.(t|j)s$": "ts-jest" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /examples/test-status-monitor/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "noImplicitAny": false, 6 | "removeComments": true, 7 | "noLib": false, 8 | "allowSyntheticDefaultImports": true, 9 | "emitDecoratorMetadata": true, 10 | "experimentalDecorators": true, 11 | "target": "es6", 12 | "sourceMap": true, 13 | "outDir": "./dist", 14 | "baseUrl": "./src" 15 | }, 16 | "include": [ 17 | "src/**/*" 18 | ], 19 | "exclude": [ 20 | "node_modules", 21 | "**/*.spec.ts" 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /examples/test-status-monitor/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tsconfig.json", 3 | "compilerOptions": { 4 | "types": ["jest", "node"] 5 | }, 6 | "include": ["**/*.spec.ts", "**/*.d.ts"] 7 | } 8 | -------------------------------------------------------------------------------- /examples/test-status-monitor/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": [ 4 | "tslint:recommended" 5 | ], 6 | "jsRules": { 7 | "no-unused-expression": true 8 | }, 9 | "rules": { 10 | "eofline": false, 11 | "quotemark": [ 12 | true, 13 | "single" 14 | ], 15 | "indent": false, 16 | "member-access": [ 17 | false 18 | ], 19 | "ordered-imports": [ 20 | false 21 | ], 22 | "max-line-length": [ 23 | true, 24 | 150 25 | ], 26 | "member-ordering": [ 27 | false 28 | ], 29 | "curly": false, 30 | "interface-name": [ 31 | false 32 | ], 33 | "array-type": [ 34 | false 35 | ], 36 | "no-empty-interface": false, 37 | "no-empty": false, 38 | "arrow-parens": false, 39 | "object-literal-sort-keys": false, 40 | "no-unused-expression": false, 41 | "max-classes-per-file": [ 42 | false 43 | ], 44 | "variable-name": [ 45 | false 46 | ], 47 | "one-line": [ 48 | false 49 | ], 50 | "one-variable-per-declaration": [ 51 | false 52 | ] 53 | }, 54 | "rulesDirectory": [] 55 | } 56 | -------------------------------------------------------------------------------- /examples/test-status-monitor/webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const path = require('path'); 3 | const nodeExternals = require('webpack-node-externals'); 4 | 5 | module.exports = { 6 | entry: ['webpack/hot/poll?1000', './src/main.hmr.ts'], 7 | watch: true, 8 | target: 'node', 9 | externals: [ 10 | nodeExternals({ 11 | whitelist: ['webpack/hot/poll?1000'], 12 | }), 13 | ], 14 | module: { 15 | rules: [ 16 | { 17 | test: /\.tsx?$/, 18 | use: 'ts-loader', 19 | exclude: /node_modules/, 20 | }, 21 | ], 22 | }, 23 | mode: "development", 24 | resolve: { 25 | extensions: ['.tsx', '.ts', '.js'], 26 | }, 27 | plugins: [ 28 | new webpack.HotModuleReplacementPlugin(), 29 | ], 30 | output: { 31 | path: path.join(__dirname, 'dist'), 32 | filename: 'server.js', 33 | }, 34 | }; 35 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nest-status-monitor", 3 | "version": "0.1.4", 4 | "description": "Realtime Monitoring for Express-based Node applications", 5 | "main": "dist/index.js", 6 | "types": "dist/index.d.ts", 7 | "repository": { 8 | "type": "git", 9 | "url": "git+ssh://git@github.com/GenFirst/nest-status-monitor.git" 10 | }, 11 | "author": "ivanvs ", 12 | "license": "MIT", 13 | "keywords": [ 14 | "nestjs", 15 | "status", 16 | "monitoring", 17 | "node" 18 | ], 19 | "devDependencies": { 20 | "@nestjs/common": "^6.0.0", 21 | "@nestjs/core": "^6.0.0", 22 | "@nestjs/testing": "^6.0.0", 23 | "@types/jest": "^23.3.1", 24 | "@types/node": "^10.12.1", 25 | "coveralls": "^3.0.9", 26 | "jest": "^25.1.0", 27 | "nestjs-config": "^1.2.2", 28 | "prettier": "^1.19.1", 29 | "supertest": "^3.3.0", 30 | "ts-jest": "^25.1.0", 31 | "typescript": "^3.0.3" 32 | }, 33 | "dependencies": { 34 | "@nestjs/websockets": "^6.11.11", 35 | "axios": "^0.19.2", 36 | "debug": "^2.6.8", 37 | "handlebars": "4.7.3", 38 | "on-headers": "^1.0.2", 39 | "pidusage": "^1.1.6", 40 | "reflect-metadata": "^0.1.13", 41 | "request-promise-native": "^1.0.8", 42 | "socket.io": "^2.0.3" 43 | }, 44 | "scripts": { 45 | "test": "jest", 46 | "coverage": "jest --coverage", 47 | "coveralls": "npm run coverage --coverageReporters=text-lcov | coveralls", 48 | "test:watch": "jest --watch", 49 | "build": "rm -rf ./dist && tsc --declaration", 50 | "format": "prettier src/**/*.ts --ignore-path ./.prettierignore --write && git status", 51 | "prepublish": "npm run format && npm run build" 52 | }, 53 | "jest": { 54 | "moduleFileExtensions": [ 55 | "js", 56 | "json", 57 | "ts" 58 | ], 59 | "rootDir": "tests", 60 | "testRegex": ".spec.ts$", 61 | "transform": { 62 | "^.+\\.(t|j)s$": "ts-jest" 63 | }, 64 | "coverageDirectory": "./coverage", 65 | "testEnvironment": "node" 66 | }, 67 | "bugs": { 68 | "url": "https://github.com/GenFirst/nest-status-monitor/issues" 69 | }, 70 | "homepage": "https://github.com/GenFirst/nest-status-monitor#readme", 71 | "directories": { 72 | "example": "examples" 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/config/chart.visibility.configuration.ts: -------------------------------------------------------------------------------- 1 | export interface ChartVisibilityConfiguration { 2 | cpu: boolean; 3 | mem: boolean; 4 | load: boolean; 5 | responseTime: boolean; 6 | rps: boolean; 7 | statusCodes: boolean; 8 | } 9 | -------------------------------------------------------------------------------- /src/config/health.check.configuration.ts: -------------------------------------------------------------------------------- 1 | export interface HealthCheckConfiguration { 2 | protocol: string; 3 | host: string; 4 | path: string; 5 | port: number; 6 | } 7 | -------------------------------------------------------------------------------- /src/config/spans.configuration.ts: -------------------------------------------------------------------------------- 1 | export interface SpansConfiguration { 2 | interval: number; 3 | retention: number; 4 | } 5 | -------------------------------------------------------------------------------- /src/config/status.monitor.configuration.ts: -------------------------------------------------------------------------------- 1 | import { HealthCheckConfiguration } from './health.check.configuration'; 2 | import { SpansConfiguration } from './spans.configuration'; 3 | import { ChartVisibilityConfiguration } from './chart.visibility.configuration'; 4 | 5 | export interface StatusMonitorConfiguration { 6 | path: string; 7 | port: number; 8 | pageTitle: string; 9 | ignoreStartsWith: string; 10 | healthChecks: HealthCheckConfiguration[]; 11 | spans: SpansConfiguration[]; 12 | chartVisibility: ChartVisibilityConfiguration; 13 | } 14 | -------------------------------------------------------------------------------- /src/default.config.ts: -------------------------------------------------------------------------------- 1 | const configuration = { 2 | title: 'Express Status', 3 | theme: 'default.css', 4 | path: '/status', 5 | spans: [ 6 | { 7 | interval: 1, 8 | retention: 60, 9 | }, 10 | { 11 | interval: 5, 12 | retention: 60, 13 | }, 14 | { 15 | interval: 15, 16 | retention: 60, 17 | }, 18 | ], 19 | port: null, 20 | websocket: null, 21 | iframe: false, 22 | chartVisibility: { 23 | cpu: true, 24 | mem: true, 25 | load: true, 26 | responseTime: true, 27 | rps: true, 28 | statusCodes: true, 29 | }, 30 | ignoreStartsWith: '/admin', 31 | healthChecks: [], 32 | }; 33 | 34 | export default configuration; 35 | -------------------------------------------------------------------------------- /src/health.check.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Inject } from '@nestjs/common'; 2 | import axios from 'axios'; 3 | import { STATUS_MONITOR_OPTIONS_PROVIDER } from './status.monitor.constants'; 4 | import { StatusMonitorConfiguration } from './config/status.monitor.configuration'; 5 | import { HealthCheckConfiguration } from './config/health.check.configuration'; 6 | 7 | @Injectable() 8 | export class HealthCheckService { 9 | healthChecks: HealthCheckConfiguration[] = []; 10 | 11 | constructor( 12 | @Inject(STATUS_MONITOR_OPTIONS_PROVIDER) config: StatusMonitorConfiguration, 13 | ) { 14 | this.healthChecks = config.healthChecks; 15 | } 16 | 17 | checkAllEndpoints() { 18 | const checkPromises = []; 19 | 20 | this.healthChecks.forEach(healthCheck => { 21 | checkPromises.push(this.checkEndpoint(healthCheck)); 22 | }); 23 | 24 | let checkResults = []; 25 | 26 | return this.allSettled(checkPromises).then(results => { 27 | results.forEach((result, index) => { 28 | if (result.state === 'rejected') { 29 | checkResults.push({ 30 | path: this.healthChecks[index].path, 31 | status: 'failed', 32 | }); 33 | } else { 34 | checkResults.push({ 35 | path: this.healthChecks[index].path, 36 | status: 'ok', 37 | }); 38 | } 39 | }); 40 | 41 | return checkResults; 42 | }); 43 | } 44 | 45 | private checkEndpoint(healthCheck): Promise { 46 | let uri = `${healthCheck.protocol}://${healthCheck.host}`; 47 | 48 | if (healthCheck.port) { 49 | uri += `:${healthCheck.port}`; 50 | } 51 | 52 | uri += healthCheck.path; 53 | 54 | //TODO (ivasiljevic) use http service instead of axios 55 | return axios({ 56 | url: uri, 57 | method: 'GET', 58 | }); 59 | } 60 | 61 | private allSettled(promises: Promise[]): Promise { 62 | let wrappedPromises = promises.map(p => 63 | Promise.resolve(p).then( 64 | val => ({ state: 'fulfilled', value: val }), 65 | err => ({ state: 'rejected', value: err }), 66 | ), 67 | ); 68 | 69 | return Promise.all(wrappedPromises); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { StatusMonitorModule } from './status.monitor.module'; 2 | import { StatusMonitorConfiguration } from './config/status.monitor.configuration'; 3 | import { ChartVisibilityConfiguration } from './config/chart.visibility.configuration'; 4 | import { HealthCheckConfiguration } from './config/health.check.configuration'; 5 | import { SpansConfiguration } from './config/spans.configuration'; 6 | 7 | export { 8 | StatusMonitorModule, 9 | StatusMonitorConfiguration, 10 | ChartVisibilityConfiguration, 11 | HealthCheckConfiguration, 12 | SpansConfiguration, 13 | }; 14 | -------------------------------------------------------------------------------- /src/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{title}} 6 | 7 | 8 | 11 | 12 | 13 | 14 |
15 |
16 | {{title}} 17 |
18 |
19 |
20 |
21 |
22 |
CPU Usage
23 |

- %

24 |
25 |
26 | 27 |
28 |
29 |
30 |
31 |
Memory Usage
32 |

- %

33 |
34 |
35 | 36 |
37 |
38 |
39 |
40 |
One Minute Load Avg
41 |

-

42 |
43 |
44 | 45 |
46 |
47 |
48 |
49 |
Response Time
50 |

-

51 |
52 |
53 | 54 |
55 |
56 |
57 |
58 |
Requests per Second
59 |

-

60 |
61 |
62 | 63 |
64 |
65 |
66 |
67 |
Status Codes
68 |
2xx
69 |
3xx
70 |
4xx
71 |
5xx
72 |
73 |
74 | 75 |
76 |
77 |
78 | {{#each healthCheckResults}} 79 |
80 |
81 |
{{path}}
82 |
83 |
84 |

{{status}}

85 |
86 |
87 | {{/each}} 88 |
89 | 92 |
93 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /src/public/javascripts/app.js: -------------------------------------------------------------------------------- 1 | /* 2 | eslint-disable no-plusplus, no-var, strict, vars-on-top, prefer-template, 3 | func-names, prefer-arrow-callback, no-loop-func 4 | */ 5 | /* global Chart, location, document, port, parseInt, io */ 6 | 7 | 'use strict'; 8 | 9 | Chart.defaults.global.defaultFontSize = 8; 10 | Chart.defaults.global.animation.duration = 500; 11 | Chart.defaults.global.legend.display = false; 12 | Chart.defaults.global.elements.line.backgroundColor = 'rgba(0,0,0,0)'; 13 | Chart.defaults.global.elements.line.borderColor = 'rgba(0,0,0,0.9)'; 14 | Chart.defaults.global.elements.line.borderWidth = 2; 15 | 16 | var socket = io( 17 | location.protocol + '//' + location.hostname + ':' + (port || location.port), 18 | ); 19 | var defaultSpan = 0; 20 | var spans = []; 21 | var statusCodesColors = ['#75D701', '#47b8e0', '#ffc952', '#E53A40']; 22 | 23 | var defaultDataset = { 24 | label: '', 25 | data: [], 26 | lineTension: 0.2, 27 | pointRadius: 0, 28 | }; 29 | 30 | var defaultOptions = { 31 | scales: { 32 | yAxes: [ 33 | { 34 | ticks: { 35 | beginAtZero: true, 36 | }, 37 | }, 38 | ], 39 | xAxes: [ 40 | { 41 | type: 'time', 42 | time: { 43 | unitStepSize: 30, 44 | }, 45 | gridLines: { 46 | display: false, 47 | }, 48 | }, 49 | ], 50 | }, 51 | tooltips: { 52 | enabled: false, 53 | }, 54 | responsive: true, 55 | maintainAspectRatio: false, 56 | animation: false, 57 | }; 58 | 59 | var createChart = function(ctx, dataset) { 60 | return new Chart(ctx, { 61 | type: 'line', 62 | data: { 63 | labels: [], 64 | datasets: dataset, 65 | }, 66 | options: defaultOptions, 67 | }); 68 | }; 69 | 70 | var addTimestamp = function(point) { 71 | return point.timestamp; 72 | }; 73 | 74 | var cpuDataset = [Object.create(defaultDataset)]; 75 | var memDataset = [Object.create(defaultDataset)]; 76 | var loadDataset = [Object.create(defaultDataset)]; 77 | var responseTimeDataset = [Object.create(defaultDataset)]; 78 | var rpsDataset = [Object.create(defaultDataset)]; 79 | 80 | var cpuStat = document.getElementById('cpuStat'); 81 | var memStat = document.getElementById('memStat'); 82 | var loadStat = document.getElementById('loadStat'); 83 | var responseTimeStat = document.getElementById('responseTimeStat'); 84 | var rpsStat = document.getElementById('rpsStat'); 85 | 86 | var cpuChartCtx = document.getElementById('cpuChart'); 87 | var memChartCtx = document.getElementById('memChart'); 88 | var loadChartCtx = document.getElementById('loadChart'); 89 | var responseTimeChartCtx = document.getElementById('responseTimeChart'); 90 | var rpsChartCtx = document.getElementById('rpsChart'); 91 | var statusCodesChartCtx = document.getElementById('statusCodesChart'); 92 | 93 | var cpuChart = createChart(cpuChartCtx, cpuDataset); 94 | var memChart = createChart(memChartCtx, memDataset); 95 | var loadChart = createChart(loadChartCtx, loadDataset); 96 | var responseTimeChart = createChart(responseTimeChartCtx, responseTimeDataset); 97 | var rpsChart = createChart(rpsChartCtx, rpsDataset); 98 | var statusCodesChart = new Chart(statusCodesChartCtx, { 99 | type: 'line', 100 | data: { 101 | labels: [], 102 | datasets: [ 103 | Object.create(defaultDataset), 104 | Object.create(defaultDataset), 105 | Object.create(defaultDataset), 106 | Object.create(defaultDataset), 107 | ], 108 | }, 109 | options: defaultOptions, 110 | }); 111 | 112 | statusCodesChart.data.datasets.forEach(function(dataset, index) { 113 | dataset.borderColor = statusCodesColors[index]; 114 | }); 115 | 116 | var charts = [ 117 | cpuChart, 118 | memChart, 119 | loadChart, 120 | responseTimeChart, 121 | rpsChart, 122 | statusCodesChart, 123 | ]; 124 | 125 | var onSpanChange = function(e) { 126 | e.target.classList.add('active'); 127 | defaultSpan = parseInt(e.target.id, 10); 128 | 129 | var otherSpans = document.getElementsByTagName('span'); 130 | 131 | for (var i = 0; i < otherSpans.length; i++) { 132 | if (otherSpans[i] !== e.target) otherSpans[i].classList.remove('active'); 133 | } 134 | 135 | socket.emit('esm_change'); 136 | }; 137 | 138 | socket.on('esm_start', function(data) { 139 | // Remove last element of Array because it contains malformed responses data. 140 | // To keep consistency we also remove os data. 141 | data[defaultSpan].responses.pop(); 142 | data[defaultSpan].os.pop(); 143 | 144 | var lastOsMetric = data[defaultSpan].os[data[defaultSpan].os.length - 1]; 145 | 146 | cpuStat.textContent = '0.0%'; 147 | if (lastOsMetric) { 148 | cpuStat.textContent = lastOsMetric.cpu.toFixed(1) + '%'; 149 | } 150 | 151 | cpuChart.data.datasets[0].data = data[defaultSpan].os.map(function(point) { 152 | return point.cpu; 153 | }); 154 | cpuChart.data.labels = data[defaultSpan].os.map(addTimestamp); 155 | 156 | memStat.textContent = '0.0MB'; 157 | if (lastOsMetric) { 158 | memStat.textContent = lastOsMetric.memory.toFixed(1) + 'MB'; 159 | } 160 | 161 | memChart.data.datasets[0].data = data[defaultSpan].os.map(function(point) { 162 | return point.memory; 163 | }); 164 | memChart.data.labels = data[defaultSpan].os.map(addTimestamp); 165 | 166 | loadStat.textContent = '0.00'; 167 | if (lastOsMetric) { 168 | loadStat.textContent = lastOsMetric.load[defaultSpan].toFixed(2); 169 | } 170 | 171 | loadChart.data.datasets[0].data = data[defaultSpan].os.map(function(point) { 172 | return point.load[0]; 173 | }); 174 | loadChart.data.labels = data[defaultSpan].os.map(addTimestamp); 175 | 176 | var lastResponseMetric = 177 | data[defaultSpan].responses[data[defaultSpan].responses.length - 1]; 178 | 179 | responseTimeStat.textContent = '0.00ms'; 180 | if (lastResponseMetric) { 181 | responseTimeStat.textContent = lastResponseMetric.mean.toFixed(2) + 'ms'; 182 | } 183 | 184 | responseTimeChart.data.datasets[0].data = data[defaultSpan].responses.map( 185 | function(point) { 186 | return point.mean; 187 | }, 188 | ); 189 | responseTimeChart.data.labels = data[defaultSpan].responses.map(addTimestamp); 190 | 191 | for (var i = 0; i < 4; i++) { 192 | statusCodesChart.data.datasets[i].data = data[defaultSpan].responses.map( 193 | function(point) { 194 | return point[i + 2]; 195 | }, 196 | ); 197 | } 198 | statusCodesChart.data.labels = data[defaultSpan].responses.map(addTimestamp); 199 | 200 | if (data[defaultSpan].responses.length >= 2) { 201 | var deltaTime = 202 | lastResponseMetric.timestamp - 203 | data[defaultSpan].responses[data[defaultSpan].responses.length - 2] 204 | .timestamp; 205 | 206 | if (deltaTime < 1) deltaTime = 1000; 207 | rpsStat.textContent = ( 208 | (lastResponseMetric.count / deltaTime) * 209 | 1000 210 | ).toFixed(2); 211 | rpsChart.data.datasets[0].data = data[defaultSpan].responses.map(function( 212 | point, 213 | ) { 214 | return (point.count / deltaTime) * 1000; 215 | }); 216 | rpsChart.data.labels = data[defaultSpan].responses.map(addTimestamp); 217 | } 218 | 219 | charts.forEach(function(chart) { 220 | chart.update(); 221 | }); 222 | 223 | var spanControls = document.getElementById('span-controls'); 224 | 225 | if (data.length !== spans.length) { 226 | data.forEach(function(span, index) { 227 | spans.push({ 228 | retention: span.retention, 229 | interval: span.interval, 230 | }); 231 | 232 | var spanNode = document.createElement('span'); 233 | var textNode = document.createTextNode( 234 | (span.retention * span.interval) / 60 + 'M', 235 | ); 236 | 237 | spanNode.appendChild(textNode); 238 | spanNode.setAttribute('id', index); 239 | spanNode.onclick = onSpanChange; 240 | spanControls.appendChild(spanNode); 241 | }); 242 | document.getElementsByTagName('span')[0].classList.add('active'); 243 | } 244 | }); 245 | 246 | socket.on('esm_stats', function(data) { 247 | if ( 248 | data.retention === spans[defaultSpan].retention && 249 | data.interval === spans[defaultSpan].interval 250 | ) { 251 | var os = data.os; 252 | var responses = data.responses; 253 | 254 | cpuStat.textContent = '0.0%'; 255 | if (os) { 256 | cpuStat.textContent = os.cpu.toFixed(1) + '%'; 257 | cpuChart.data.datasets[0].data.push(os.cpu); 258 | cpuChart.data.labels.push(os.timestamp); 259 | } 260 | 261 | memStat.textContent = '0.0MB'; 262 | if (os) { 263 | memStat.textContent = os.memory.toFixed(1) + 'MB'; 264 | memChart.data.datasets[0].data.push(os.memory); 265 | memChart.data.labels.push(os.timestamp); 266 | } 267 | 268 | loadStat.textContent = '0'; 269 | if (os) { 270 | loadStat.textContent = os.load[0].toFixed(2); 271 | loadChart.data.datasets[0].data.push(os.load[0]); 272 | loadChart.data.labels.push(os.timestamp); 273 | } 274 | 275 | responseTimeStat.textContent = '0.00ms'; 276 | if (responses) { 277 | responseTimeStat.textContent = responses.mean.toFixed(2) + 'ms'; 278 | responseTimeChart.data.datasets[0].data.push(responses.mean); 279 | responseTimeChart.data.labels.push(responses.timestamp); 280 | } 281 | 282 | if (responses) { 283 | var deltaTime = 284 | responses.timestamp - 285 | rpsChart.data.labels[rpsChart.data.labels.length - 1]; 286 | 287 | if (deltaTime < 1) deltaTime = 1000; 288 | rpsStat.textContent = ((responses.count / deltaTime) * 1000).toFixed(2); 289 | rpsChart.data.datasets[0].data.push((responses.count / deltaTime) * 1000); 290 | rpsChart.data.labels.push(responses.timestamp); 291 | } 292 | 293 | if (responses) { 294 | for (var i = 0; i < 4; i++) { 295 | statusCodesChart.data.datasets[i].data.push(data.responses[i + 2]); 296 | } 297 | statusCodesChart.data.labels.push(data.responses.timestamp); 298 | } 299 | 300 | charts.forEach(function(chart) { 301 | if (spans[defaultSpan].retention < chart.data.labels.length) { 302 | chart.data.datasets.forEach(function(dataset) { 303 | dataset.data.shift(); 304 | }); 305 | 306 | chart.data.labels.shift(); 307 | } 308 | chart.update(); 309 | }); 310 | } 311 | }); 312 | -------------------------------------------------------------------------------- /src/public/stylesheets/style.css: -------------------------------------------------------------------------------- 1 | * { 2 | font-family: Helvetica Neue, Helvetica, Arial, sans-serif; 3 | } 4 | 5 | body.hide-cpu .container.cpu, 6 | body.hide-mem .container.mem, 7 | body.hide-load .container.load, 8 | body.hide-responseTime .container.responseTime, 9 | body.hide-rps .container.rps, 10 | body.hide-statusCodes .container.statusCodes { 11 | display: none; 12 | } 13 | 14 | h1 { 15 | font-size: 3em; 16 | color: #222; 17 | margin: 0; 18 | } 19 | 20 | h5 { 21 | margin: 0; 22 | color: #888; 23 | } 24 | 25 | h6 { 26 | margin: 0; 27 | } 28 | 29 | p { 30 | font-size: 0.7em; 31 | color: #888; 32 | } 33 | 34 | span { 35 | cursor: pointer; 36 | font-size: 10px; 37 | margin-left: 5px; 38 | border: 1px solid #ddd; 39 | padding: 3px 10px 4px 10px; 40 | } 41 | 42 | canvas { 43 | width: 400px; 44 | height: 100px; 45 | } 46 | 47 | .content { 48 | width: 600px; 49 | margin: auto; 50 | } 51 | 52 | .active { 53 | background: #eeeeee; 54 | } 55 | 56 | .stats-column { 57 | flex: 0 0 200px; 58 | } 59 | 60 | .container { 61 | display: flex; 62 | flex-direction: row; 63 | margin-top: 20px; 64 | height: 100px; 65 | } 66 | 67 | .chart-container { 68 | width: 400px; 69 | height: 100px; 70 | } 71 | 72 | .container.healthChecks { 73 | display: block; 74 | height: auto; 75 | } 76 | 77 | .health-check-row { 78 | align-items: center; 79 | border: 1px solid #eee; 80 | border-radius: 4px; 81 | display: flex; 82 | margin: 0 0 10px 0; 83 | width: 100%; 84 | } 85 | 86 | .health-check-title-column { 87 | flex: 0 0 400px; 88 | display: flex; 89 | align-items: center; 90 | padding: 0 10px; 91 | } 92 | 93 | .health-check-title-column h5 a { 94 | color: #888; 95 | cursor: pointer; 96 | text-decoration: none; 97 | } 98 | 99 | .health-check-title-column h5 a:hover { 100 | text-decoration: underline; 101 | } 102 | 103 | .health-check-status-container { 104 | align-items: center; 105 | border-radius: 0 4px 4px 0; 106 | display: flex; 107 | justify-content: center; 108 | height: 2em; 109 | text-align: center; 110 | width: 200px; 111 | } 112 | 113 | .health-check-status-container.ok { 114 | background: #75d701; 115 | } 116 | 117 | .health-check-status-container.failed { 118 | background: #e53a40; 119 | } 120 | 121 | .health-check-status-container h1 { 122 | line-height: 2em; 123 | font-size: 1.5em; 124 | color: #fff; 125 | text-align: center; 126 | text-transform: uppercase; 127 | } 128 | 129 | .footer { 130 | text-align: center; 131 | } 132 | 133 | .span-controls { 134 | float: right; 135 | } 136 | 137 | .status-code { 138 | margin-top: 2px; 139 | } 140 | 141 | .status-code:before { 142 | content: ''; 143 | display: inline-block; 144 | width: 8px; 145 | height: 8px; 146 | border-radius: 8px; 147 | margin-right: 10px; 148 | } 149 | 150 | .status-code-2xx:before { 151 | background-color: #75d701; 152 | } 153 | 154 | .status-code-3xx:before { 155 | background-color: #47b8e0; 156 | } 157 | 158 | .status-code-4xx:before { 159 | background-color: #ffc952; 160 | } 161 | 162 | .status-code-5xx:before { 163 | background-color: #e53a40; 164 | } 165 | -------------------------------------------------------------------------------- /src/status.monitor.constants.ts: -------------------------------------------------------------------------------- 1 | export const STATUS_MONITOR_OPTIONS_PROVIDER = 2 | 'STATUS_MONITOR_OPTIONS_PROVIDER'; 3 | -------------------------------------------------------------------------------- /src/status.monitor.controller.ts: -------------------------------------------------------------------------------- 1 | import { Get, Controller, HttpCode, Inject } from '@nestjs/common'; 2 | import * as fs from 'fs'; 3 | import * as path from 'path'; 4 | import { HealthCheckService } from './health.check.service'; 5 | import { PATH_METADATA } from '@nestjs/common/constants'; 6 | import { STATUS_MONITOR_OPTIONS_PROVIDER } from './status.monitor.constants'; 7 | import { StatusMonitorConfiguration } from './config/status.monitor.configuration'; 8 | const Handlebars = require('handlebars'); 9 | 10 | @Controller() 11 | export class StatusMonitorController { 12 | data; 13 | render; 14 | 15 | constructor( 16 | private readonly healtCheckService: HealthCheckService, 17 | @Inject(STATUS_MONITOR_OPTIONS_PROVIDER) config: StatusMonitorConfiguration, 18 | ) { 19 | const bodyClasses = Object.keys(config.chartVisibility) 20 | .reduce((accumulator, key) => { 21 | if (config.chartVisibility[key] === false) { 22 | accumulator.push(`hide-${key}`); 23 | } 24 | return accumulator; 25 | }, []) 26 | .join(' '); 27 | 28 | this.data = { 29 | title: config.pageTitle, 30 | port: config.port, 31 | bodyClasses: bodyClasses, 32 | script: fs.readFileSync( 33 | path.join(__dirname, '../src/public/javascripts/app.js'), 34 | ), 35 | style: fs.readFileSync( 36 | path.join(__dirname, '../src/public/stylesheets/style.css'), 37 | ), 38 | }; 39 | 40 | const htmlTmpl = fs 41 | .readFileSync(path.join(__dirname, '../src/public/index.html')) 42 | .toString(); 43 | 44 | this.render = Handlebars.compile(htmlTmpl, { strict: true }); 45 | } 46 | 47 | public static forRoot(rootPath: string = 'monitor') { 48 | Reflect.defineMetadata(PATH_METADATA, rootPath, StatusMonitorController); 49 | return StatusMonitorController; 50 | } 51 | 52 | @Get() 53 | @HttpCode(200) 54 | async root() { 55 | const healtData = await this.healtCheckService.checkAllEndpoints(); 56 | this.data.healthCheckResults = healtData; 57 | return this.render(this.data); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/status.monitor.gateway.ts: -------------------------------------------------------------------------------- 1 | import { 2 | SubscribeMessage, 3 | WebSocketGateway, 4 | WebSocketServer, 5 | OnGatewayConnection, 6 | } from '@nestjs/websockets'; 7 | import { StatusMonitoringService } from './status.monitoring.service'; 8 | import { Inject, forwardRef } from '@nestjs/common'; 9 | 10 | @WebSocketGateway() 11 | export class StatusMonitorGateway implements OnGatewayConnection { 12 | @WebSocketServer() 13 | server; 14 | 15 | constructor( 16 | @Inject(forwardRef(() => StatusMonitoringService)) 17 | private readonly statusMonitoringService: StatusMonitoringService, 18 | ) {} 19 | 20 | @SubscribeMessage('esm_change') 21 | onEvent(client, data: any) { 22 | const event = 'esm_start'; 23 | const spans = this.statusMonitoringService.getData(); 24 | return { event, spans }; 25 | } 26 | 27 | handleConnection(client) { 28 | const spans = this.statusMonitoringService.getData(); 29 | client.emit('esm_start', spans); 30 | } 31 | 32 | sendMetrics(metrics) { 33 | if (this.server) { 34 | const data = { 35 | os: metrics.os[metrics.os.length - 2], 36 | responses: metrics.responses[metrics.responses.length - 2], 37 | interval: metrics.interval, 38 | retention: metrics.retention, 39 | }; 40 | this.server.emit('esm_stats', data); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/status.monitor.middleware.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Injectable, 3 | NestMiddleware, 4 | Inject, 5 | } from '@nestjs/common'; 6 | import * as onHeaders from 'on-headers'; 7 | import { StatusMonitoringService } from './status.monitoring.service'; 8 | import { STATUS_MONITOR_OPTIONS_PROVIDER } from './status.monitor.constants'; 9 | import { StatusMonitorConfiguration } from './config/status.monitor.configuration'; 10 | 11 | @Injectable() 12 | export class StatusMonitorMiddleware implements NestMiddleware { 13 | constructor( 14 | private readonly statusMonitoringService: StatusMonitoringService, 15 | @Inject(STATUS_MONITOR_OPTIONS_PROVIDER) 16 | private readonly config: StatusMonitorConfiguration, 17 | ) {} 18 | 19 | use(req, res, next: Function) { 20 | if ( 21 | this.config.ignoreStartsWith && 22 | !req.originalUrl.startsWith(this.config.ignoreStartsWith) && 23 | !req.originalUrl.startsWith(this.config.path) 24 | ) { 25 | const startTime = process.hrtime(); 26 | onHeaders(res, () => { 27 | this.statusMonitoringService.collectResponseTime( 28 | res.statusCode, 29 | startTime, 30 | ); 31 | }); 32 | } 33 | 34 | next(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/status.monitor.module.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Module, 3 | MiddlewareConsumer, 4 | RequestMethod, 5 | DynamicModule, 6 | } from '@nestjs/common'; 7 | import { StatusMonitorController } from './status.monitor.controller'; 8 | import { StatusMonitorGateway } from './status.monitor.gateway'; 9 | import { StatusMonitoringService } from './status.monitoring.service'; 10 | import { StatusMonitorMiddleware } from './status.monitor.middleware'; 11 | import { HealthCheckService } from './health.check.service'; 12 | import { StatusMonitorConfiguration } from './config/status.monitor.configuration'; 13 | import { STATUS_MONITOR_OPTIONS_PROVIDER } from './status.monitor.constants'; 14 | 15 | @Module({ 16 | controllers: [StatusMonitorController.forRoot('monitor')], 17 | providers: [ 18 | StatusMonitorGateway, 19 | StatusMonitoringService, 20 | HealthCheckService, 21 | ], 22 | }) 23 | export class StatusMonitorModule { 24 | configure(consumer: MiddlewareConsumer) { 25 | consumer 26 | .apply(StatusMonitorMiddleware) 27 | .forRoutes({ path: '*', method: RequestMethod.ALL }); 28 | } 29 | 30 | static setUp(config: StatusMonitorConfiguration): DynamicModule { 31 | return { 32 | module: StatusMonitorModule, 33 | providers: [ 34 | { 35 | provide: STATUS_MONITOR_OPTIONS_PROVIDER, 36 | useValue: config, 37 | }, 38 | StatusMonitorGateway, 39 | StatusMonitoringService, 40 | HealthCheckService, 41 | ], 42 | controllers: [StatusMonitorController.forRoot(config.path)], 43 | }; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/status.monitoring.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Inject, forwardRef } from '@nestjs/common'; 2 | import * as pidusage from 'pidusage'; 3 | import * as os from 'os'; 4 | import { StatusMonitorGateway } from './status.monitor.gateway'; 5 | import { STATUS_MONITOR_OPTIONS_PROVIDER } from './status.monitor.constants'; 6 | import { StatusMonitorConfiguration } from './config/status.monitor.configuration'; 7 | 8 | @Injectable() 9 | export class StatusMonitoringService { 10 | spans = []; 11 | 12 | constructor( 13 | @Inject(forwardRef(() => StatusMonitorGateway)) 14 | private readonly statusMonitorWs: StatusMonitorGateway, 15 | @Inject(STATUS_MONITOR_OPTIONS_PROVIDER) 16 | readonly config: StatusMonitorConfiguration, 17 | ) { 18 | config.spans.forEach(currentSpan => { 19 | const span = { 20 | os: [], 21 | responses: [], 22 | interval: currentSpan.interval, 23 | retention: currentSpan.retention, 24 | }; 25 | 26 | this.spans.push(span); 27 | 28 | const interval = setInterval(() => { 29 | this.collectOsMetrics(span); 30 | this.sendOsMetrics(span); 31 | }, span.interval * 1000); 32 | interval.unref(); // don't keep node.js process up 33 | }); 34 | } 35 | 36 | collectOsMetrics(span) { 37 | const defaultResponse = { 38 | 2: 0, 39 | 3: 0, 40 | 4: 0, 41 | 5: 0, 42 | count: 0, 43 | mean: 0, 44 | timestamp: Date.now(), 45 | }; 46 | 47 | pidusage.stat(process.pid, (err, stat) => { 48 | if (err) { 49 | return; 50 | } 51 | 52 | const last = span.responses[span.responses.length - 1]; 53 | 54 | // Convert from B to MB 55 | stat.memory = stat.memory / 1024 / 1024; 56 | stat.load = os.loadavg(); 57 | stat.timestamp = Date.now(); 58 | 59 | span.os.push(stat); 60 | if ( 61 | !span.responses[0] || 62 | last.timestamp + span.interval * 1000 < Date.now() 63 | ) { 64 | span.responses.push(defaultResponse); 65 | } 66 | 67 | // todo: I think this check should be moved somewhere else 68 | if (span.os.length >= span.retention) span.os.shift(); 69 | if (span.responses[0] && span.responses.length > span.retention) 70 | span.responses.shift(); 71 | }); 72 | } 73 | 74 | sendOsMetrics(span) { 75 | this.statusMonitorWs.sendMetrics(span); 76 | } 77 | 78 | getData() { 79 | return this.spans; 80 | } 81 | 82 | collectResponseTime(statusCode, startTime) { 83 | const diff = process.hrtime(startTime); 84 | const responseTime = (diff[0] * 1e3 + diff[1]) * 1e-6; 85 | const category = Math.floor(statusCode / 100); 86 | 87 | this.spans.forEach(span => { 88 | const last = span.responses[span.responses.length - 1]; 89 | 90 | if ( 91 | last !== undefined && 92 | last.timestamp / 1000 + span.interval > Date.now() / 1000 93 | ) { 94 | last[category] += 1; 95 | last.count += 1; 96 | last.mean += (responseTime - last.mean) / last.count; 97 | } else { 98 | span.responses.push({ 99 | 2: category === 2 ? 1 : 0, 100 | 3: category === 3 ? 1 : 0, 101 | 4: category === 4 ? 1 : 0, 102 | 5: category === 5 ? 1 : 0, 103 | count: 1, 104 | mean: responseTime, 105 | timestamp: Date.now(), 106 | }); 107 | } 108 | }); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /tests/setup.spec.ts: -------------------------------------------------------------------------------- 1 | describe('Component', () => { 2 | it('should be able to run tests', () => { 3 | expect(1 + 2).toEqual(3); 4 | }); 5 | }); 6 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "moduleResolution": "node", 4 | "module": "commonjs", 5 | "declaration": true, 6 | "noImplicitAny": false, 7 | "noUnusedLocals": false, 8 | "removeComments": false, 9 | "noLib": false, 10 | "emitDecoratorMetadata": true, 11 | "experimentalDecorators": true, 12 | "target": "es6", 13 | "sourceMap": false, 14 | "allowJs": false, 15 | "rootDir": "./src", 16 | "baseUrl": "./", 17 | "outDir": "./dist", 18 | "lib": ["es2017"] 19 | }, 20 | "include": ["*.ts", "**/*.ts"], 21 | "exclude": ["node_modules", "./**/*.spec.ts", "examples"] 22 | } 23 | --------------------------------------------------------------------------------