├── .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 | [](https://nodei.co/npm/nest-status-monitor/)
4 |
5 | [](https://www.npmjs.com/package/nest-status-monitor)
6 | [](https://img.shields.io/npm/dt/nest-status-monitor.svg)
7 | [](https://travis-ci.org/GenFirst/nest-status-monitor)
8 | [](https://coveralls.io/github/GenFirst/nest-status-monitor?branch=master)
9 | 
10 | [](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 | 
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 |
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 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
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 |
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 |
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 |
--------------------------------------------------------------------------------