├── .codacy.yml ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── dependabot.yml └── workflows │ ├── ci.yml │ └── codacy-analysis.yml ├── .gitignore ├── .npmignore ├── .prettierignore ├── .prettierrc ├── CODE_OF_CONDUCT.md ├── LICENSE ├── Procfile ├── README.md ├── SECURITY.md ├── examples └── integrate-status-monitor │ ├── .prettierrc │ ├── nest-cli.json │ ├── package-lock.json │ ├── package.json │ ├── src │ ├── app.module.ts │ ├── healthController.ts │ └── main.ts │ ├── test │ ├── app.e2e-spec.ts │ └── jest-e2e.json │ ├── tsconfig.build.json │ ├── tsconfig.json │ ├── tsconfig.spec.json │ └── tslint.json ├── 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 │ │ └── default.css ├── status.monitor.constants.ts ├── status.monitor.controller.ts ├── status.monitor.gateway.ts ├── status.monitor.middleware.ts ├── status.monitor.module.ts └── status.monitor.service.ts ├── test ├── jest-e2e.json └── status-monitor.e2e-spec.ts └── tsconfig.json /.codacy.yml: -------------------------------------------------------------------------------- 1 | --- 2 | exclude_paths: 3 | - '.bundle/**' 4 | - 'spec/**/*' 5 | - 'benchmarks/**/*' 6 | - '**.min.js' 7 | - '**/tests/**' 8 | - '.github/**/*' 9 | - '**.md' 10 | - '**.css' -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | 4 | - package-ecosystem: "npm" 5 | directory: "/" 6 | schedule: 7 | interval: "monthly" 8 | labels: 9 | - version-update 10 | 11 | - package-ecosystem: "npm" 12 | directory: "/examples/integrate-status-monitor" 13 | schedule: 14 | interval: "monthly" 15 | labels: 16 | - version-update -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [ main ] 9 | pull_request: 10 | branches: [ main ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | node-version: [10.x, 12.x, 14.x, 15.x] 20 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 21 | 22 | steps: 23 | - uses: actions/checkout@v2 24 | - name: Use Node.js ${{ matrix.node-version }} 25 | uses: actions/setup-node@v1 26 | with: 27 | node-version: ${{ matrix.node-version }} 28 | - run: npm ci 29 | - run: npm run build --if-present 30 | - run: npm test 31 | 32 | - run: npm ci 33 | working-directory: ./examples/integrate-status-monitor 34 | - run: npm run build --if-present 35 | working-directory: ./examples/integrate-status-monitor 36 | - run: npm run test:e2e 37 | working-directory: ./examples/integrate-status-monitor 38 | 39 | automerge: 40 | needs: build 41 | runs-on: ubuntu-latest 42 | permissions: 43 | pull-requests: write 44 | contents: write 45 | steps: 46 | - uses: fastify/github-action-merge-dependabot@v3.0.0 47 | if: ${{ github.actor == 'dependabot[bot]' && github.event_name == 'pull_request' }} 48 | with: 49 | github-token: ${{secrets.GITHUB_TOKEN}} 50 | merge-method: 'squash' 51 | -------------------------------------------------------------------------------- /.github/workflows/codacy-analysis.yml: -------------------------------------------------------------------------------- 1 | # This workflow checks out code, performs a Codacy security scan 2 | # and integrates the results with the 3 | # GitHub Advanced Security code scanning feature. For more information on 4 | # the Codacy security scan action usage and parameters, see 5 | # https://github.com/codacy/codacy-analysis-cli-action. 6 | # For more information on Codacy Analysis CLI in general, see 7 | # https://github.com/codacy/codacy-analysis-cli. 8 | 9 | name: Codacy Security Scan 10 | 11 | on: 12 | push: 13 | branches: [ main ] 14 | pull_request: 15 | branches: [ main ] 16 | 17 | jobs: 18 | codacy-security-scan: 19 | name: Codacy Security Scan 20 | runs-on: ubuntu-latest 21 | steps: 22 | # Checkout the repository to the GitHub Actions runner 23 | - name: Checkout code 24 | uses: actions/checkout@v2 25 | 26 | # Execute Codacy Analysis CLI and generate a SARIF output with the security issues identified during the analysis 27 | - name: Run Codacy Analysis CLI 28 | uses: codacy/codacy-analysis-cli-action@1.1.0 29 | with: 30 | # Check https://github.com/codacy/codacy-analysis-cli#project-token to get your project token from your Codacy repository 31 | # You can also omit the token and run the tools that support default configurations 32 | project-token: ${{ secrets.CODACY_PROJECT_TOKEN }} 33 | verbose: true 34 | output: results.sarif 35 | format: sarif 36 | # Adjust severity of non-security issues 37 | gh-code-scanning-compat: true 38 | # Force 0 exit code to allow SARIF file generation 39 | # This will handover control about PR rejection to the GitHub side 40 | max-allowed-issues: 2147483647 41 | 42 | # Upload the SARIF file generated in the previous step 43 | - name: Upload SARIF results file 44 | uses: github/codeql-action/upload-sarif@v1 45 | with: 46 | sarif_file: results.sarif 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | coverage 4 | .DS_Store 5 | nestjs-status-monitor-*.tgz -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .prettierignore 3 | .prettierrc 4 | .travis.yml 5 | .gitignore 6 | .github 7 | .codacy.yml 8 | Procfile 9 | 10 | 11 | tsconfig.json 12 | *.ts 13 | !*.d.ts 14 | nestjs-status-monitor-*.tgz 15 | 16 | examples 17 | node_modules 18 | test 19 | coverage -------------------------------------------------------------------------------- /.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 | } -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at honnamkuan@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Hon Nam, Kuan 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. -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: node ./examples/integrate-status-monitor/dist/main.js -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nestjs-status-monitor 2 | 3 | [![NPM](https://nodei.co/npm/nestjs-status-monitor.png?downloads=true&downloadRank=true&stars=true)](https://nodei.co/npm/nestjs-status-monitor/) 4 | 5 | [![nestjs-status-monitor on npm](https://img.shields.io/npm/v/nestjs-status-monitor.svg)](https://www.npmjs.com/package/nestjs-status-monitor) 6 | ![David](https://img.shields.io/david/honnamkuan/nestjs-status-monitor) 7 | [![Codacy Badge](https://app.codacy.com/project/badge/Grade/35443212eca84c2c94dd6dcfe4170ab3)](https://www.codacy.com/gh/honnamkuan/nestjs-status-monitor/dashboard?utm_source=github.com&utm_medium=referral&utm_content=honnamkuan/nestjs-status-monitor&utm_campaign=Badge_Grade) 8 | ![GitHub Workflow Status](https://img.shields.io/github/workflow/status/honnamkuan/nestjs-status-monitor/Node.js%20CI) 9 | [![npm](https://img.shields.io/npm/dt/nestjs-status-monitor.svg)](https://img.shields.io/npm/dt/nestjs-status-monitor.svg) 10 | ![GitHub](https://img.shields.io/github/license/honnamkuan/nestjs-status-monitor) 11 | 12 | Simple, self-hosted module based on Socket.io and Chart.js to report realtime server metrics for NestJS v7+ based servers. 13 | 14 | ![Status monitor page](https://i.imgur.com/1xlO8lM.gif 'Status monitor page') 15 | 16 | ## Live Demo 17 | 18 | [Demo available here](https://nestjs-status-monitor.herokuapp.com/status) 19 | 20 | ## Installation & setup NestJS v7 21 | 22 | 1. Run `npm install nestjs-status-monitor --save` 23 | 2. Setup module import: 24 | 25 | ```javascript 26 | @Module({ 27 | imports: [StatusMonitorModule.forRoot()] //default config 28 | }) 29 | ``` 30 | 31 | 3. Run server and visit `/status` 32 | 33 | ## Options 34 | 35 | Monitor can be configured by passing options object during initialization of 36 | module. 37 | 38 | ```javascript 39 | @Module({ 40 | imports: [StatusMonitorModule.forRoot(config)] 41 | }) 42 | ``` 43 | 44 | Default config: 45 | 46 | ```javascript 47 | { 48 | title: 'NestJS Status', // Default title 49 | path: '/status', 50 | socketPath: '/socket.io', // In case you use a custom path 51 | port: null, // Defaults to NestJS port 52 | spans: [ 53 | { 54 | interval: 1, // Every second 55 | retention: 60, // Keep 60 datapoints in memory 56 | }, 57 | { 58 | interval: 5, // Every 5 seconds 59 | retention: 60, 60 | }, 61 | { 62 | interval: 15, // Every 15 seconds 63 | retention: 60, 64 | }, 65 | ], 66 | chartVisibility: { 67 | cpu: true, 68 | mem: true, 69 | load: true, 70 | eventLoop: true, 71 | heap: true, 72 | responseTime: true, 73 | rps: true, 74 | statusCodes: true, 75 | }, 76 | ignoreStartsWith: ['/admin'], // paths to ignore for responseTime stats 77 | healthChecks: [], 78 | } 79 | ``` 80 | 81 | ## Health Checks 82 | 83 | You can add a series of health checks to the configuration that will appear 84 | below the other stats. The health check will be considered successful if the 85 | endpoint returns a 200 status code. 86 | 87 | ```javascript 88 | // config 89 | healthChecks: [ 90 | { 91 | protocol: 'http', 92 | host: 'localhost', 93 | path: '/health/alive', 94 | port: 3001, 95 | }, 96 | { 97 | protocol: 'http', 98 | host: 'localhost', 99 | path: '/health/dead', 100 | port: 3001, 101 | }, 102 | ]; 103 | ``` 104 | 105 | ## Local demo 106 | 107 | 1. Run the following: 108 | 109 | ```sh 110 | npm i 111 | cd examples/integrate-status-monitor 112 | npm i 113 | npm start 114 | ``` 115 | 116 | 2. Visit [http://localhost:3001/status](http://localhost:3001/status) 117 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | Versions currently being supported with security updates. 6 | 7 | | Version | Supported | 8 | | ------- | ------------------ | 9 | | 1.0.x | :white_check_mark: | 10 | -------------------------------------------------------------------------------- /examples/integrate-status-monitor/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } -------------------------------------------------------------------------------- /examples/integrate-status-monitor/nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "language": "ts", 3 | "collection": "@nestjs/schematics", 4 | "sourceRoot": "src" 5 | } 6 | -------------------------------------------------------------------------------- /examples/integrate-status-monitor/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "integrate-status-monitor", 3 | "version": "1.0.2", 4 | "description": "Example how to use status monitor module", 5 | "main": "dist/main.js", 6 | "author": "honnamkuan", 7 | "license": "MIT", 8 | "scripts": { 9 | "prebuild": "rimraf dist", 10 | "build": "nest build", 11 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", 12 | "start": "nest start", 13 | "start:dev": "nest start --watch", 14 | "start:debug": "nest start --debug --watch", 15 | "start:prod": "node dist/main", 16 | "lint": "tslint -p tsconfig.json -c tslint.json", 17 | "test": "jest", 18 | "test:watch": "jest --watch", 19 | "test:cov": "jest --coverage", 20 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", 21 | "test:e2e": "jest --config ./test/jest-e2e.json" 22 | }, 23 | "dependencies": { 24 | "@nestjs/core": "^7.6.11", 25 | "@nestjs/platform-express": "^7.6.18", 26 | "@nestjs/platform-socket.io": "^9.4.0", 27 | "@nestjs/websockets": "^7.6.15", 28 | "socket.io": "^4.6.1", 29 | "@nestjs/common": "^7.6.18" 30 | }, 31 | "devDependencies": { 32 | "@nestjs/cli": "^9.1.5", 33 | "@nestjs/testing": "^7.6.15", 34 | "@types/jest": "^26.0.20", 35 | "@types/node": "^18.11.9", 36 | "@types/supertest": "^2.0.12", 37 | "@typescript-eslint/eslint-plugin": "^4.33.0", 38 | "@typescript-eslint/parser": "^4.33.0", 39 | "eslint": "^7.32.0", 40 | "eslint-config-prettier": "^8.5.0", 41 | "eslint-plugin-prettier": "^4.2.1", 42 | "jest": "^26.6.3", 43 | "prettier": "^2.8.1", 44 | "rimraf": "^3.0.2", 45 | "supertest": "^6.3.3", 46 | "ts-jest": "^26.5.6", 47 | "ts-loader": "^9.4.2", 48 | "ts-node": "^10.9.1", 49 | "tsconfig-paths": "^4.1.2", 50 | "tslint": "6.1.3", 51 | "typescript": "^4.9.5", 52 | "webpack": "^5.76.0", 53 | "webpack-cli": "^5.0.1", 54 | "webpack-node-externals": "^3.0.0" 55 | }, 56 | "jest": { 57 | "moduleFileExtensions": [ 58 | "js", 59 | "json", 60 | "ts" 61 | ], 62 | "rootDir": "src", 63 | "testRegex": ".spec.ts$", 64 | "transform": { 65 | "^.+\\.(t|j)s$": "ts-jest" 66 | }, 67 | "coverageDirectory": "../coverage", 68 | "testEnvironment": "node" 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /examples/integrate-status-monitor/src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { 3 | StatusMonitorModule, 4 | StatusMonitorConfiguration, 5 | } from '../../../dist/index'; 6 | import { HealthController } from './healthController'; 7 | 8 | const SOCKET_IO_PORT = +process.env.EXTERNAL_PORT || +process.env.PORT || 3001; 9 | const APP_PORT = +process.env.PORT || 3001; 10 | 11 | const statusMonitorConfig: StatusMonitorConfiguration = { 12 | title: 'NestJS Monitoring Page', 13 | port: SOCKET_IO_PORT, 14 | socketPath: '/socket.io', 15 | path: '/status', 16 | ignoreStartsWith: '/admin', 17 | healthChecks: [ 18 | { 19 | protocol: 'http', 20 | host: 'localhost', 21 | path: '/admin/health/alive', 22 | port: APP_PORT, 23 | }, 24 | { 25 | protocol: 'http', 26 | host: 'localhost', 27 | path: '/admin/health/dead', 28 | port: APP_PORT, 29 | }, 30 | ], 31 | spans: [ 32 | { 33 | interval: 1, // Every second 34 | retention: 60, // Keep 60 datapoints in memory 35 | }, 36 | { 37 | interval: 5, // Every 5 seconds 38 | retention: 60, 39 | }, 40 | { 41 | interval: 15, // Every 15 seconds 42 | retention: 60, 43 | }, 44 | ], 45 | chartVisibility: { 46 | cpu: true, 47 | mem: true, 48 | load: true, 49 | responseTime: true, 50 | rps: true, 51 | statusCodes: true, 52 | }, 53 | }; 54 | 55 | @Module({ 56 | imports: [StatusMonitorModule.forRoot(statusMonitorConfig)], 57 | controllers: [HealthController], 58 | providers: [], 59 | }) 60 | export class AppModule {} 61 | -------------------------------------------------------------------------------- /examples/integrate-status-monitor/src/healthController.ts: -------------------------------------------------------------------------------- 1 | import { Get, Controller, HttpCode } from '@nestjs/common'; 2 | 3 | @Controller('admin/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/integrate-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 | import { Logger } from '@nestjs/common'; 5 | 6 | const logger = new Logger('bootstrap'); 7 | 8 | async function bootstrap() { 9 | const app = await NestFactory.create(AppModule); 10 | const PORT = +process.env.PORT || 3001; 11 | await app.listen(PORT); 12 | logger.log(`Access status monitor at http://localhost:${PORT}/status`); 13 | } 14 | bootstrap(); 15 | -------------------------------------------------------------------------------- /examples/integrate-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('/admin/health/alive returns 200', () => { 19 | return request(app.getHttpServer()) 20 | .get('/admin/health/alive') 21 | .expect(200) 22 | .expect('OK'); 23 | }); 24 | 25 | it('/admin/health/dead returns 500', () => { 26 | return request(app.getHttpServer()) 27 | .get('/admin/health/dead') 28 | .expect(500) 29 | .expect('DEAD'); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /examples/integrate-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/integrate-status-monitor/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["./../../dist", "./../../src/public", "src"], 4 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 5 | } 6 | -------------------------------------------------------------------------------- /examples/integrate-status-monitor/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "moduleResolution": "node", 4 | "module": "commonjs", 5 | "declaration": true, 6 | "noImplicitAny": false, 7 | "removeComments": true, 8 | "noLib": false, 9 | "allowSyntheticDefaultImports": true, 10 | "emitDecoratorMetadata": true, 11 | "experimentalDecorators": true, 12 | "target": "es6", 13 | "sourceMap": false, 14 | "outDir": "./dist", 15 | "rootDir": "./src", 16 | "baseUrl": "./", 17 | "lib": ["es2017"] 18 | }, 19 | "typeAcquisition": { "enable": true } 20 | } 21 | -------------------------------------------------------------------------------- /examples/integrate-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/integrate-status-monitor/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": ["tslint:recommended"], 4 | "jsRules": { 5 | "no-unused-expression": true 6 | }, 7 | "rules": { 8 | "eofline": false, 9 | "quotemark": [true, "single"], 10 | "indent": false, 11 | "member-access": [false], 12 | "ordered-imports": [false], 13 | "max-line-length": [true, 150], 14 | "member-ordering": [false], 15 | "curly": false, 16 | "interface-name": [false], 17 | "array-type": [false], 18 | "no-empty-interface": false, 19 | "no-empty": false, 20 | "arrow-parens": false, 21 | "object-literal-sort-keys": false, 22 | "no-unused-expression": false, 23 | "max-classes-per-file": [false], 24 | "variable-name": [false], 25 | "one-line": [false], 26 | "one-variable-per-declaration": [false] 27 | }, 28 | "rulesDirectory": [] 29 | } 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nestjs-status-monitor", 3 | "version": "1.0.1", 4 | "description": "Realtime Monitoring for Express-based NestJS applications", 5 | "main": "dist/index.js", 6 | "types": "dist/index.d.ts", 7 | "repository": { 8 | "type": "git", 9 | "url": "git+https://github.com/honnamkuan/nestjs-status-monitor.git" 10 | }, 11 | "author": "honnamkuan", 12 | "license": "MIT", 13 | "keywords": [ 14 | "nestjs", 15 | "status", 16 | "monitoring", 17 | "node" 18 | ], 19 | "devDependencies": { 20 | "@nestjs/common": "^7.6.11", 21 | "@nestjs/core": "^7.6.11", 22 | "@nestjs/platform-express": "^7.6.11", 23 | "@nestjs/platform-socket.io": "^7.6.11", 24 | "@nestjs/testing": "^7.6.11", 25 | "@types/jest": "^26.0.20", 26 | "@types/node": "^18.0.0", 27 | "@types/supertest": "^2.0.10", 28 | "coveralls": "^3.1.0", 29 | "jest": "^26.6.3", 30 | "nestjs-config": "^1.4.7", 31 | "prettier": "^2.2.1", 32 | "socket.io-client": "^2.4.0", 33 | "supertest": "^6.1.3", 34 | "ts-jest": "^26.5.0", 35 | "typescript": "^4.1.3" 36 | }, 37 | "dependencies": { 38 | "@nestjs/websockets": "^7.6.11", 39 | "axios": "^0.27.2", 40 | "handlebars": "4.7.7", 41 | "on-headers": "^1.0.2", 42 | "pidusage": "^3.0.0", 43 | "socket.io": "^4.1.1" 44 | }, 45 | "scripts": { 46 | "test": "jest", 47 | "coverage": "jest --coverage", 48 | "coveralls": "npm run coverage --coverageReporters=text-lcov | coveralls", 49 | "test:watch": "jest --watch", 50 | "start:dev": "tsc --watch --declaration", 51 | "build": "rm -rf ./dist && tsc --declaration", 52 | "format": "prettier src/**/*.ts --ignore-path ./.prettierignore", 53 | "prepare": "npm run format && npm run build", 54 | "heroku-postbuild": "npm run build && cd ./examples/integrate-status-monitor && npm ci && npm run build" 55 | }, 56 | "jest": { 57 | "moduleFileExtensions": [ 58 | "js", 59 | "json", 60 | "ts" 61 | ], 62 | "rootDir": "test", 63 | "testRegex": ".spec.ts$", 64 | "transform": { 65 | "^.+\\.(t|j)s$": "ts-jest" 66 | }, 67 | "coverageDirectory": "./coverage", 68 | "testEnvironment": "node" 69 | }, 70 | "bugs": { 71 | "url": "https://github.com/honnamkuan/nestjs-status-monitor/issues" 72 | }, 73 | "homepage": "https://github.com/honnamkuan/nestjs-status-monitor#readme", 74 | "directories": { 75 | "example": "examples", 76 | "test": "test" 77 | }, 78 | "optionalDependencies": { 79 | "event-loop-stats": "^1.3.0" 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /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 | socketPath: string; 9 | title: string; 10 | ignoreStartsWith: string | string[]; 11 | healthChecks: HealthCheckConfiguration[]; 12 | spans: SpansConfiguration[]; 13 | chartVisibility: ChartVisibilityConfiguration; 14 | } 15 | -------------------------------------------------------------------------------- /src/default.config.ts: -------------------------------------------------------------------------------- 1 | const configuration = { 2 | title: 'NestJS Status', // Default title 3 | path: '/status', 4 | socketPath: '/socket.io', // In case you use a custom path 5 | port: null, 6 | spans: [ 7 | { 8 | interval: 1, // Every second 9 | retention: 60, // Keep 60 datapoints in memory 10 | }, 11 | { 12 | interval: 5, // Every 5 seconds 13 | retention: 60, 14 | }, 15 | { 16 | interval: 15, // Every 15 seconds 17 | retention: 60, 18 | }, 19 | ], 20 | chartVisibility: { 21 | cpu: true, 22 | mem: true, 23 | load: true, 24 | eventLoop: true, 25 | heap: true, 26 | responseTime: true, 27 | rps: true, 28 | statusCodes: true, 29 | }, 30 | healthChecks: [], 31 | ignoreStartsWith: ['/admin'], 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 | return axios({ 55 | url: uri, 56 | method: 'GET', 57 | }); 58 | } 59 | 60 | private allSettled(promises: Promise[]): Promise { 61 | let wrappedPromises = promises.map(p => 62 | Promise.resolve(p).then( 63 | val => ({ state: 'fulfilled', value: val }), 64 | err => ({ state: 'rejected', value: err }), 65 | ), 66 | ); 67 | 68 | return Promise.all(wrappedPromises); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /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 | {{title}} 5 | 6 | 7 | 10 | 11 | 12 |
13 |
14 | {{title}} 15 |
16 |
17 |
18 |
19 |
CPU Usage
20 |

- %

21 |
22 |
23 | 24 |
25 |
26 |
27 |
28 |
Memory Usage
29 |

- %

30 |
31 |
32 | 33 |
34 |
35 |
36 |
37 |
Heap Usage
38 |

- %

39 |
40 |
41 | 42 |
43 |
44 |
45 |
46 |
One Minute Load Avg
47 |

-

48 |
49 |
50 | 51 |
52 |
53 |
54 |
55 |
Spent in Event Loop
56 |

ms

57 |
58 |
59 | 60 |
61 |
62 |
63 |
64 |
Response Time
65 |

-

66 |
67 |
68 | 69 |
70 |
71 |
72 |
73 |
Requests per Second
74 |

-

75 |
76 |
77 | 78 |
79 |
80 |
81 |
82 |
Status Codes
83 |
2xx
84 |
3xx
85 |
4xx
86 |
5xx
87 |
88 |
89 | 90 |
91 |
92 |
93 | {{#each healthCheckResults}} 94 |
95 |
96 |
{{path}}
97 |
98 |
99 |

{{status}}

100 |
101 |
102 | {{/each}} 103 |
104 |
105 | 110 | 111 | -------------------------------------------------------------------------------- /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, socketPath, 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 serverUrl = location.protocol + '//' + location.hostname + ':' + (port || location.port); 17 | var socket = io(serverUrl + '/status-monitor'); 18 | fetchAndRefresh(); 19 | 20 | var defaultSpan = 0; 21 | var spans = []; 22 | var statusCodesColors = ['#75D701', '#47b8e0', '#ffc952', '#E53A40']; 23 | 24 | var defaultDataset = { 25 | label: '', 26 | data: [], 27 | lineTension: 0.2, 28 | pointRadius: 0, 29 | }; 30 | 31 | var defaultOptions = { 32 | scales: { 33 | yAxes: [ 34 | { 35 | ticks: { 36 | beginAtZero: true, 37 | }, 38 | }, 39 | ], 40 | xAxes: [ 41 | { 42 | type: 'time', 43 | time: { 44 | unitStepSize: 30, 45 | }, 46 | gridLines: { 47 | display: false, 48 | }, 49 | }, 50 | ], 51 | }, 52 | tooltips: { 53 | enabled: false, 54 | }, 55 | responsive: true, 56 | maintainAspectRatio: false, 57 | animation: false, 58 | }; 59 | 60 | var createChart = function (ctx, dataset) { 61 | return new Chart(ctx, { 62 | type: 'line', 63 | data: { 64 | labels: [], 65 | datasets: dataset, 66 | }, 67 | options: defaultOptions, 68 | }); 69 | }; 70 | 71 | var addTimestamp = function (point) { 72 | return point.timestamp; 73 | }; 74 | 75 | var cpuDataset = [Object.create(defaultDataset)]; 76 | var memDataset = [Object.create(defaultDataset)]; 77 | var loadDataset = [Object.create(defaultDataset)]; 78 | var heapDataset = [Object.create(defaultDataset)]; 79 | var eventLoopDataset = [Object.create(defaultDataset)]; 80 | var responseTimeDataset = [Object.create(defaultDataset)]; 81 | var rpsDataset = [Object.create(defaultDataset)]; 82 | 83 | var cpuStat = document.getElementById('cpuStat'); 84 | var memStat = document.getElementById('memStat'); 85 | var loadStat = document.getElementById('loadStat'); 86 | var heapStat = document.getElementById('heapStat'); 87 | var eventLoopStat = document.getElementById('eventLoopStat'); 88 | var responseTimeStat = document.getElementById('responseTimeStat'); 89 | var rpsStat = document.getElementById('rpsStat'); 90 | 91 | var cpuChartCtx = document.getElementById('cpuChart'); 92 | var memChartCtx = document.getElementById('memChart'); 93 | var loadChartCtx = document.getElementById('loadChart'); 94 | var heapChartCtx = document.getElementById('heapChart'); 95 | var eventLoopChartCtx = document.getElementById('eventLoopChart'); 96 | var responseTimeChartCtx = document.getElementById('responseTimeChart'); 97 | var rpsChartCtx = document.getElementById('rpsChart'); 98 | var statusCodesChartCtx = document.getElementById('statusCodesChart'); 99 | 100 | var cpuChart = createChart(cpuChartCtx, cpuDataset); 101 | var memChart = createChart(memChartCtx, memDataset); 102 | var heapChart = createChart(heapChartCtx, heapDataset); 103 | var eventLoopChart = createChart(eventLoopChartCtx, eventLoopDataset); 104 | var loadChart = createChart(loadChartCtx, loadDataset); 105 | var responseTimeChart = createChart(responseTimeChartCtx, responseTimeDataset); 106 | var rpsChart = createChart(rpsChartCtx, rpsDataset); 107 | var statusCodesChart = new Chart(statusCodesChartCtx, { 108 | type: 'line', 109 | data: { 110 | labels: [], 111 | datasets: [ 112 | Object.create(defaultDataset), 113 | Object.create(defaultDataset), 114 | Object.create(defaultDataset), 115 | Object.create(defaultDataset), 116 | ], 117 | }, 118 | options: defaultOptions, 119 | }); 120 | 121 | statusCodesChart.data.datasets.forEach(function (dataset, index) { 122 | dataset.borderColor = statusCodesColors[index]; 123 | }); 124 | 125 | var charts = [ 126 | cpuChart, 127 | memChart, 128 | loadChart, 129 | responseTimeChart, 130 | rpsChart, 131 | statusCodesChart, 132 | heapChart, 133 | eventLoopChart, 134 | ]; 135 | 136 | var onSpanChange = function (e) { 137 | e.target.classList.add('active'); 138 | defaultSpan = parseInt(e.target.id, 10); 139 | 140 | var otherSpans = document.getElementsByTagName('span'); 141 | 142 | for (var i = 0; i < otherSpans.length; i++) { 143 | if (otherSpans[i] !== e.target) otherSpans[i].classList.remove('active'); 144 | } 145 | 146 | fetchAndRefresh(); 147 | }; 148 | 149 | function fetchAndRefresh(){ 150 | fetch(serverUrl + location.pathname + '/data') 151 | .then(response => response.json()) 152 | .then(refreshData) 153 | .catch(console.error); 154 | } 155 | 156 | function refreshData(data) { 157 | // Remove last element of Array because it contains malformed responses data. 158 | // To keep consistency we also remove os data. 159 | data[defaultSpan].responses.pop(); 160 | data[defaultSpan].os.pop(); 161 | 162 | var lastOsMetric = data[defaultSpan].os[data[defaultSpan].os.length - 1]; 163 | 164 | cpuStat.textContent = '0.0%'; 165 | if (lastOsMetric) { 166 | cpuStat.textContent = lastOsMetric.cpu.toFixed(1) + '%'; 167 | } 168 | 169 | cpuChart.data.datasets[0].data = data[defaultSpan].os.map(function (point) { 170 | return point.cpu; 171 | }); 172 | cpuChart.data.labels = data[defaultSpan].os.map(addTimestamp); 173 | 174 | memStat.textContent = '0.0MB'; 175 | if (lastOsMetric) { 176 | memStat.textContent = lastOsMetric.memory.toFixed(1) + 'MB'; 177 | } 178 | 179 | memChart.data.datasets[0].data = data[defaultSpan].os.map(function (point) { 180 | return point.memory; 181 | }); 182 | memChart.data.labels = data[defaultSpan].os.map(addTimestamp); 183 | 184 | loadStat.textContent = '0.00'; 185 | if (lastOsMetric && lastOsMetric.load[0]) { 186 | loadStat.textContent = lastOsMetric.load[0].toFixed(2); 187 | } 188 | 189 | loadChart.data.datasets[0].data = data[defaultSpan].os.map(function (point) { 190 | return point.load[0]; 191 | }); 192 | loadChart.data.labels = data[defaultSpan].os.map(addTimestamp); 193 | 194 | heapChart.data.datasets[0].data = data[defaultSpan].os.map(function (point) { 195 | return point.heap.used_heap_size / 1024 / 1024; 196 | }); 197 | heapChart.data.labels = data[defaultSpan].os.map(addTimestamp); 198 | 199 | eventLoopChart.data.datasets[0].data = data[defaultSpan].os.map(function (point) { 200 | if (point.loop) { 201 | return point.loop.sum; 202 | } 203 | return 0; 204 | }); 205 | eventLoopChart.data.labels = data[defaultSpan].os.map(addTimestamp); 206 | 207 | var lastResponseMetric = data[defaultSpan].responses[data[defaultSpan].responses.length - 1]; 208 | 209 | responseTimeStat.textContent = '0ms'; 210 | if (lastResponseMetric) { 211 | responseTimeStat.textContent = lastResponseMetric.mean.toFixed(0) + 'ms'; 212 | } 213 | 214 | responseTimeChart.data.datasets[0].data = data[defaultSpan].responses.map(function (point) { 215 | return point.mean; 216 | }); 217 | responseTimeChart.data.labels = data[defaultSpan].responses.map(addTimestamp); 218 | 219 | for (var i = 0; i < 4; i++) { 220 | statusCodesChart.data.datasets[i].data = data[defaultSpan].responses.map(function (point) { 221 | return point[i + 2]; 222 | }); 223 | } 224 | statusCodesChart.data.labels = data[defaultSpan].responses.map(addTimestamp); 225 | 226 | if (data[defaultSpan].responses.length >= 2) { 227 | var deltaTime = 228 | lastResponseMetric.timestamp - 229 | data[defaultSpan].responses[data[defaultSpan].responses.length - 2].timestamp; 230 | 231 | if (deltaTime < 1) deltaTime = 1000; 232 | rpsStat.textContent = ((lastResponseMetric.count / deltaTime) * 1000).toFixed(2); 233 | rpsChart.data.datasets[0].data = data[defaultSpan].responses.map(function (point) { 234 | return (point.count / deltaTime) * 1000; 235 | }); 236 | rpsChart.data.labels = data[defaultSpan].responses.map(addTimestamp); 237 | } 238 | 239 | charts.forEach(function (chart) { 240 | chart.update(); 241 | }); 242 | 243 | var spanControls = document.getElementById('span-controls'); 244 | 245 | if (data.length !== spans.length) { 246 | data.forEach(function (span, index) { 247 | spans.push({ 248 | retention: span.retention, 249 | interval: span.interval, 250 | }); 251 | 252 | var spanNode = document.createElement('span'); 253 | var textNode = document.createTextNode((span.retention * span.interval) / 60 + 'M'); // eslint-disable-line 254 | 255 | spanNode.appendChild(textNode); 256 | spanNode.setAttribute('id', index); 257 | spanNode.onclick = onSpanChange; 258 | spanControls.appendChild(spanNode); 259 | }); 260 | document.getElementsByTagName('span')[0].classList.add('active'); 261 | } 262 | } 263 | 264 | socket.on('esm_stats', function (data) { 265 | console.log(data); 266 | 267 | if ( 268 | data.retention === spans[defaultSpan].retention && 269 | data.interval === spans[defaultSpan].interval 270 | ) { 271 | var os = data.os; 272 | var responses = data.responses; 273 | 274 | cpuStat.textContent = '0.0%'; 275 | if (os) { 276 | cpuStat.textContent = os.cpu.toFixed(1) + '%'; 277 | cpuChart.data.datasets[0].data.push(os.cpu); 278 | cpuChart.data.labels.push(os.timestamp); 279 | } 280 | 281 | memStat.textContent = '0.0MB'; 282 | if (os) { 283 | memStat.textContent = os.memory.toFixed(1) + 'MB'; 284 | memChart.data.datasets[0].data.push(os.memory); 285 | memChart.data.labels.push(os.timestamp); 286 | } 287 | 288 | loadStat.textContent = '0'; 289 | if (os) { 290 | loadStat.textContent = os.load[0].toFixed(2); 291 | loadChart.data.datasets[0].data.push(os.load[0]); 292 | loadChart.data.labels.push(os.timestamp); 293 | } 294 | 295 | heapStat.textContent = '0'; 296 | if (os) { 297 | heapStat.textContent = (os.heap.used_heap_size / 1024 / 1024).toFixed(1) + 'MB'; 298 | heapChart.data.datasets[0].data.push(os.heap.used_heap_size / 1024 / 1024); 299 | heapChart.data.labels.push(os.timestamp); 300 | } 301 | 302 | eventLoopStat.textContent = '0'; 303 | if (os && os.loop) { 304 | eventLoopStat.textContent = os.loop.sum.toFixed(0) + 'ms'; 305 | eventLoopChart.data.datasets[0].data.push(os.loop.sum); 306 | eventLoopChart.data.labels.push(os.timestamp); 307 | } 308 | 309 | responseTimeStat.textContent = '0'; 310 | if (responses) { 311 | responseTimeStat.textContent = responses.mean.toFixed(0) + 'ms'; 312 | responseTimeChart.data.datasets[0].data.push(responses.mean); 313 | responseTimeChart.data.labels.push(responses.timestamp); 314 | } 315 | 316 | if (responses) { 317 | var deltaTime = responses.timestamp - rpsChart.data.labels[rpsChart.data.labels.length - 1]; 318 | 319 | if (deltaTime < 1) deltaTime = 1000; 320 | rpsStat.textContent = ((responses.count / deltaTime) * 1000).toFixed(2); 321 | rpsChart.data.datasets[0].data.push((responses.count / deltaTime) * 1000); 322 | rpsChart.data.labels.push(responses.timestamp); 323 | } 324 | 325 | if (responses) { 326 | for (var i = 0; i < 4; i++) { 327 | statusCodesChart.data.datasets[i].data.push(data.responses[i + 2]); 328 | } 329 | statusCodesChart.data.labels.push(data.responses.timestamp); 330 | } 331 | 332 | charts.forEach(function (chart) { 333 | if (spans[defaultSpan].retention < chart.data.labels.length) { 334 | chart.data.datasets.forEach(function (dataset) { 335 | dataset.data.shift(); 336 | }); 337 | 338 | chart.data.labels.shift(); 339 | } 340 | chart.update(); 341 | }); 342 | } 343 | }); -------------------------------------------------------------------------------- /src/public/stylesheets/default.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 | } -------------------------------------------------------------------------------- /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 | import { StatusMonitorService } from './status.monitor.service'; 9 | const Handlebars = require('handlebars'); 10 | 11 | @Controller() 12 | export class StatusMonitorController { 13 | data; 14 | render; 15 | 16 | constructor( 17 | private readonly healthCheckService: HealthCheckService, 18 | @Inject(STATUS_MONITOR_OPTIONS_PROVIDER) config: StatusMonitorConfiguration, 19 | private readonly statusMonitorService :StatusMonitorService 20 | ) { 21 | const bodyClasses = Object.keys(config.chartVisibility) 22 | .reduce((accumulator, key) => { 23 | if (config.chartVisibility[key] === false) { 24 | accumulator.push(`hide-${key}`); 25 | } 26 | return accumulator; 27 | }, []) 28 | .join(' '); 29 | 30 | this.data = { 31 | title: config.title, 32 | port: config.port, 33 | socketPath: config.socketPath, 34 | bodyClasses: bodyClasses, 35 | script: fs.readFileSync( 36 | path.join(__dirname, '../src/public/javascripts/app.js'), 37 | ), 38 | style: fs.readFileSync( 39 | path.join(__dirname, '../src/public/stylesheets/', 'default.css'), 40 | ), 41 | }; 42 | 43 | const htmlTmpl = fs 44 | .readFileSync(path.join(__dirname, '../src/public/index.html')) 45 | .toString(); 46 | 47 | this.render = Handlebars.compile(htmlTmpl, { strict: true }); 48 | } 49 | 50 | public static forRoot(rootPath: string = 'monitor') { 51 | Reflect.defineMetadata(PATH_METADATA, rootPath, StatusMonitorController); 52 | return StatusMonitorController; 53 | } 54 | 55 | @Get() 56 | @HttpCode(200) 57 | async root() { 58 | const healthData = await this.healthCheckService.checkAllEndpoints(); 59 | this.data.healthCheckResults = healthData; 60 | return this.render(this.data); 61 | } 62 | 63 | @Get('/data') 64 | @HttpCode(200) 65 | async getData(){ 66 | return this.statusMonitorService.getData(); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/status.monitor.gateway.ts: -------------------------------------------------------------------------------- 1 | import { 2 | WebSocketGateway, 3 | WebSocketServer, 4 | } from '@nestjs/websockets'; 5 | 6 | @WebSocketGateway({ namespace: 'status-monitor' }) 7 | export class StatusMonitorGateway { 8 | @WebSocketServer() 9 | server; 10 | 11 | sendMetrics(metrics) { 12 | if (this.server) { 13 | const data = { 14 | os: metrics.os[metrics.os.length - 2], 15 | responses: metrics.responses[metrics.responses.length - 2], 16 | interval: metrics.interval, 17 | retention: metrics.retention, 18 | }; 19 | this.server.emit('esm_stats', data); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/status.monitor.middleware.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, NestMiddleware, Inject } from '@nestjs/common'; 2 | import * as onHeaders from 'on-headers'; 3 | import { StatusMonitorService } from './status.monitor.service'; 4 | import { STATUS_MONITOR_OPTIONS_PROVIDER } from './status.monitor.constants'; 5 | import { StatusMonitorConfiguration } from './config/status.monitor.configuration'; 6 | 7 | @Injectable() 8 | export class StatusMonitorMiddleware implements NestMiddleware { 9 | constructor( 10 | private readonly statusMonitorService: StatusMonitorService, 11 | @Inject(STATUS_MONITOR_OPTIONS_PROVIDER) 12 | private readonly config: StatusMonitorConfiguration, 13 | ) {} 14 | 15 | use(req, res, next: Function) { 16 | let ignoredStartWithList: string[] = []; 17 | if (this.config.ignoreStartsWith) { 18 | const isArray = Array.isArray(this.config.ignoreStartsWith); 19 | ignoredStartWithList = isArray 20 | ? this.config.ignoreStartsWith as any 21 | : [this.config.ignoreStartsWith]; 22 | } 23 | if ( 24 | !req.originalUrl.startsWith(this.config.path) && 25 | ignoredStartWithList.every(i => !req.originalUrl.startsWith(i)) 26 | ) { 27 | const startTime = process.hrtime(); 28 | onHeaders(res, () => { 29 | this.statusMonitorService.collectResponseTime( 30 | res.statusCode, 31 | startTime, 32 | ); 33 | }); 34 | } 35 | 36 | next(); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/status.monitor.module.ts: -------------------------------------------------------------------------------- 1 | import { 2 | MiddlewareConsumer, 3 | RequestMethod, 4 | DynamicModule, 5 | } from '@nestjs/common'; 6 | import { StatusMonitorController } from './status.monitor.controller'; 7 | import { StatusMonitorGateway } from './status.monitor.gateway'; 8 | import { StatusMonitorService } from './status.monitor.service'; 9 | import { StatusMonitorMiddleware } from './status.monitor.middleware'; 10 | import { HealthCheckService } from './health.check.service'; 11 | import { StatusMonitorConfiguration } from './config/status.monitor.configuration'; 12 | import { STATUS_MONITOR_OPTIONS_PROVIDER } from './status.monitor.constants'; 13 | import * as defaultConfig from './default.config'; 14 | 15 | export class StatusMonitorModule { 16 | configure(consumer: MiddlewareConsumer) { 17 | consumer 18 | .apply(StatusMonitorMiddleware) 19 | .forRoutes({ path: '*', method: RequestMethod.ALL }); 20 | } 21 | 22 | static forRoot( 23 | config: StatusMonitorConfiguration = defaultConfig.default, 24 | ): DynamicModule { 25 | return { 26 | module: StatusMonitorModule, 27 | providers: [ 28 | { 29 | provide: STATUS_MONITOR_OPTIONS_PROVIDER, 30 | useValue: config, 31 | }, 32 | StatusMonitorGateway, 33 | StatusMonitorService, 34 | HealthCheckService, 35 | ], 36 | controllers: [StatusMonitorController.forRoot(config.path)], 37 | }; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/status.monitor.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Inject, forwardRef, Logger } 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 | import * as v8 from 'v8'; 8 | 9 | @Injectable() 10 | export class StatusMonitorService { 11 | spans = []; 12 | eventLoopStats; 13 | 14 | constructor( 15 | @Inject(forwardRef(() => StatusMonitorGateway)) 16 | private readonly statusMonitorWs: StatusMonitorGateway, 17 | @Inject(STATUS_MONITOR_OPTIONS_PROVIDER) 18 | readonly config: StatusMonitorConfiguration, 19 | ) { 20 | import('event-loop-stats') 21 | .then((module) => (this.eventLoopStats = module)) 22 | .catch((err) => 23 | Logger.warn( 24 | 'event-loop-stats not found, ignoring event loop metrics...', 25 | ), 26 | ); 27 | 28 | config.spans.forEach((currentSpan) => { 29 | const span = { 30 | os: [], 31 | responses: [], 32 | interval: currentSpan.interval, 33 | retention: currentSpan.retention, 34 | }; 35 | 36 | this.spans.push(span); 37 | 38 | this.collectOsMetrics(span); 39 | this.sendOsMetrics(span); 40 | 41 | const interval = setInterval(() => { 42 | this.collectOsMetrics(span); 43 | this.sendOsMetrics(span); 44 | }, span.interval * 1000); 45 | interval.unref(); // don't keep node.js process up 46 | }); 47 | } 48 | 49 | collectOsMetrics(span) { 50 | const defaultResponse = { 51 | 2: 0, 52 | 3: 0, 53 | 4: 0, 54 | 5: 0, 55 | count: 0, 56 | mean: 0, 57 | timestamp: Date.now(), 58 | }; 59 | 60 | pidusage(process.pid, (err, stat) => { 61 | if (err) { 62 | Logger.debug(err, this.constructor.name); 63 | return; 64 | } 65 | 66 | const last = span.responses[span.responses.length - 1]; 67 | 68 | // Convert from B to MB 69 | stat.memory = stat.memory / 1024 / 1024; 70 | stat.load = os.loadavg(); 71 | stat.timestamp = Date.now(); 72 | const { used_heap_size } = v8.getHeapStatistics(); 73 | stat.heap = { used_heap_size }; 74 | 75 | if (this.eventLoopStats && this.eventLoopStats.sense) { 76 | stat.loop = this.eventLoopStats.sense(); 77 | } 78 | 79 | span.os.push(stat); 80 | if ( 81 | !span.responses[0] || 82 | last.timestamp + span.interval * 1000 < Date.now() 83 | ) { 84 | span.responses.push(defaultResponse); 85 | } 86 | 87 | // todo: I think this check should be moved somewhere else 88 | if (span.os.length >= span.retention) span.os.shift(); 89 | if (span.responses[0] && span.responses.length > span.retention) 90 | span.responses.shift(); 91 | }); 92 | } 93 | 94 | sendOsMetrics(span) { 95 | this.statusMonitorWs.sendMetrics(span); 96 | } 97 | 98 | getData() { 99 | return this.spans; 100 | } 101 | 102 | collectResponseTime(statusCode, startTime) { 103 | const diff = process.hrtime(startTime); 104 | const responseTime = (diff[0] * 1e3 + diff[1]) * 1e-6; 105 | const category = Math.floor(statusCode / 100); 106 | 107 | this.spans.forEach((span) => { 108 | const last = span.responses[span.responses.length - 1]; 109 | 110 | if ( 111 | last !== undefined && 112 | last.timestamp / 1000 + span.interval > Date.now() / 1000 113 | ) { 114 | last[category] += 1; 115 | last.count += 1; 116 | last.mean += (responseTime - last.mean) / last.count; 117 | } else { 118 | span.responses.push({ 119 | 2: category === 2 ? 1 : 0, 120 | 3: category === 3 ? 1 : 0, 121 | 4: category === 4 ? 1 : 0, 122 | 5: category === 5 ? 1 : 0, 123 | count: 1, 124 | mean: responseTime, 125 | timestamp: Date.now(), 126 | }); 127 | } 128 | }); 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /test/status-monitor.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import * as request from 'supertest'; 2 | import { Test } from '@nestjs/testing'; 3 | import { connect } from 'socket.io-client'; 4 | import { StatusMonitorModule } from '../src/status.monitor.module'; 5 | import { INestApplication } from '@nestjs/common'; 6 | import { StatusMonitorService } from '../src/status.monitor.service'; 7 | import { StatusMonitorGateway } from '../src/status.monitor.gateway'; 8 | 9 | describe('Status Monitor Module (e2e)', () => { 10 | let app: INestApplication; 11 | let gateway: StatusMonitorGateway; 12 | let statusMonitorService = { 13 | collectResponseTime: () => {}, 14 | getData: () => { 15 | return [ 16 | { 17 | os: [ 18 | { 19 | cpu: 0, 20 | memory: 53.2265625, 21 | timestamp: 1612493738893, 22 | load: [0, 0, 0], 23 | heap: { 24 | used_heap_size: 20338712, 25 | }, 26 | }, 27 | { 28 | cpu: 1, 29 | memory: 54.2265625, 30 | timestamp: 1612493739894, 31 | load: [0.2, 1, 0.8], 32 | heap: { 33 | used_heap_size: 22338712, 34 | }, 35 | }, 36 | ], 37 | responses: [ 38 | { 39 | '2': 0, 40 | '3': 0, 41 | '4': 0, 42 | '5': 0, 43 | count: 0, 44 | mean: 0, 45 | timestamp: 1612493735889, 46 | }, 47 | { 48 | '2': 1, 49 | '3': 0, 50 | '4': 1, 51 | '5': 0, 52 | count: 2, 53 | mean: 2, 54 | timestamp: 1612493739894, 55 | }, 56 | ], 57 | interval: 1, 58 | retention: 60, 59 | }, 60 | ]; 61 | }, 62 | }; 63 | 64 | beforeEach(async () => { 65 | const moduleRef = await Test.createTestingModule({ 66 | imports: [StatusMonitorModule.forRoot()], 67 | }) 68 | .overrideProvider(StatusMonitorService) 69 | .useValue(statusMonitorService) 70 | .compile(); 71 | 72 | app = moduleRef.createNestApplication(); 73 | await app.init(); 74 | await app.listenAsync(3000); 75 | gateway = app.get(StatusMonitorGateway); 76 | }); 77 | 78 | describe(`GET /status/data`, () => { 79 | it('should returns 200 with stats', () => 80 | request(app.getHttpServer()) 81 | .get('/status/data') 82 | .expect(200) 83 | .expect(statusMonitorService.getData()) 84 | .expect('Content-type', /json/)); 85 | }); 86 | 87 | describe(`GET /status`, () => { 88 | it(`should returns html`, () => { 89 | request(app.getHttpServer()) 90 | .get('/status') 91 | .expect(200) 92 | .expect('Content-type', /html/); 93 | }); 94 | }); 95 | 96 | describe(`Gateway initialized`, () => { 97 | it(`gateway should be defined`, () => { 98 | expect(gateway).toBeDefined(); 99 | }); 100 | 101 | describe(`Metric sent`, () => { 102 | it(`client should receive metrics`, async (done) => { 103 | const client = connect('http://localhost:3000/status-monitor'); 104 | client.on('connect', () => { 105 | const metricData = statusMonitorService.getData()[0]; 106 | gateway.sendMetrics(metricData); 107 | client.on('esm_stats', (data) => { 108 | const expected = { 109 | ...metricData, 110 | os: metricData.os.slice(-2, -1)[0], 111 | responses: metricData.responses.slice(-2, -1)[0], 112 | }; 113 | expect(data).toEqual(expected); 114 | client.disconnect(); 115 | done(); 116 | }); 117 | }); 118 | }); 119 | }); 120 | }); 121 | 122 | afterEach(async () => { 123 | await app.close(); 124 | }); 125 | }); 126 | -------------------------------------------------------------------------------- /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", "dist"], 22 | "typeAcquisition": { 23 | "enable": true 24 | } 25 | } 26 | --------------------------------------------------------------------------------