├── .github └── workflows │ ├── nodeci.yml │ └── npmpublish.yml ├── .gitignore ├── LICENSE ├── README.md ├── example ├── example.js └── package.json ├── index.ts ├── lib └── monitor.ts ├── package.json ├── test ├── app_test.js ├── swagger │ └── api │ │ ├── swagger-config.yml │ │ └── users.js └── test_monitor.js └── tsconfig.json /.github/workflows/nodeci.yml: -------------------------------------------------------------------------------- 1 | name: Node.js CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | container-job: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | strategy: 11 | matrix: 12 | node-version: [12.1, 13.7.0] 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Use Node.js 17 | uses: actions/setup-node@v1 18 | with: 19 | node-version: '12.x' 20 | - run: npm install 21 | - run: npm test 22 | -------------------------------------------------------------------------------- /.github/workflows/npmpublish.yml: -------------------------------------------------------------------------------- 1 | name: Node.js Package 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | - uses: actions/setup-node@v1 13 | with: 14 | node-version: 12 15 | - run: npm i 16 | 17 | publish-npm: 18 | needs: build 19 | runs-on: ubuntu-latest 20 | steps: 21 | - uses: actions/checkout@v2 22 | - uses: actions/setup-node@v1 23 | with: 24 | node-version: 12 25 | registry-url: https://registry.npmjs.org/ 26 | - run: npm i 27 | - run: npm publish 28 | env: 29 | NODE_AUTH_TOKEN: ${{secrets.npm_token}} 30 | 31 | publish-gpr: 32 | needs: build 33 | runs-on: ubuntu-latest 34 | steps: 35 | - uses: actions/checkout@v2 36 | - uses: actions/setup-node@v1 37 | with: 38 | node-version: 12 39 | registry-url: https://npm.pkg.github.com/ 40 | scope: '@labbsr0x' 41 | - run: npm i 42 | - run: npm publish 43 | env: 44 | NODE_AUTH_TOKEN: ${{secrets.GITHUB_TOKEN}} 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | 63 | # package-lock 64 | package-lock.json 65 | 66 | .idea 67 | 68 | dist/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Labbs 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 | # express-monitor 2 | 3 | A Prometheus middleware to add basic but very useful metrics for your Express JS app. 4 | 5 | # Metrics 6 | 7 | The only exposed metrics (for now) are the following: 8 | 9 | ``` 10 | request_seconds_bucket{type, status, isError, errorMessage, method, addr, le} 11 | request_seconds_count{type, status, isError, errorMessage, method, addr} 12 | request_seconds_sum{type, status, isError, errorMessage, method, addr} 13 | response_size_bytes{type, status, isError, errorMessage, method, addr} 14 | dependency_up{name} 15 | dependency_request_seconds_bucket{name, type, status, isError, errorMessage, method, addr, le} 16 | dependency_request_seconds_count{name, type, status, isError, errorMessage, method, add} 17 | dependency_request_seconds_sum{name, type, status, isError, errorMessage, method, add} 18 | application_info{version} 19 | ``` 20 | 21 | Where, for a specific request, `type` tells which request protocol was used (`grpc`, `http`, etc), `status` registers the response status, `method` registers the request method, `addr` registers the requested endpoint address, `version` tells which version of your app handled the request, `isError` lets us know if the status code reported is an error or not, and `name` register the name of the dependency. 22 | 23 | In detail: 24 | 25 | 1. `request_seconds_bucket` is a metric defines the histogram of how many requests are falling into the well defined buckets represented by the label `le`; 26 | 27 | 2. `request_seconds_count` is a counter that counts the overall number of requests with those exact label occurrences; 28 | 29 | 3. `request_seconds_sum` is a counter that counts the overall sum of how long the requests with those exact label occurrences are taking; 30 | 31 | 4. `response_size_bytes` is a counter that computes how much data is being sent back to the user for a given request type. It captures the response size from the `content-length` response header. If there is no such header, the value exposed as metric will be zero; 32 | 33 | 5. `dependency_up` is a metric to register weather a specific dependency is up (1) or down (0). The label `name` registers the dependency name; 34 | 35 | 6. The `dependency_request_seconds_bucket` is a metric that defines the histogram of how many requests to a specific dependency are falling into the well defined buckets represented by the label le; 36 | 37 | 7. The `dependency_request_seconds_count` is a counter that counts the overall number of requests to a specific dependency; 38 | 39 | 8. The `dependency_request_seconds_sum` is a counter that counts the overall sum of how long requests to a specific dependency are taking; 40 | 41 | 9. Finally, `application_info` holds static info of an application, such as it's semantic version number; 42 | 43 | # How to 44 | 45 | Add this package as a dependency: 46 | 47 | ``` 48 | npm i -P @labbsr0x/express-monitor@2.11.0 49 | ``` 50 | 51 | ## HTTP Metrics 52 | 53 | Use it as middleware: 54 | 55 | ```js 56 | const express = require("express"); 57 | const { Monitor } = require("@labbsr0x/express-monitor"); 58 | 59 | const app = express(); 60 | Monitor.init(app, true); // the 'true' argument exposes default NodeJS metrics as well 61 | ``` 62 | 63 | One can optionally define the buckets of observation for the `request_second` histogram by doing: 64 | 65 | ```js 66 | ... 67 | Monitor.init(app, true, [0.1]); // where only one bucket (of 100ms) will be given as output in the /metrics endpoint 68 | ``` 69 | 70 | Other optional parameters are also: 71 | 1. `version`: a semantic version string identifying the version of your application. Empty by default. 72 | 2. `isErrorCallback`: an error callback to define what **you** consider as error. `4**` and `5**` considered as errors by default; 73 | 3. `metricsEndpoint`: the endpoint where the metrics will be exposed. `/metrics` by default. 74 | 75 | `Monitor` also comes with a `promclient` so you can expose your custom prometheus metrics: 76 | 77 | ```js 78 | // below we define a Gauge metric 79 | var myGauge = new Monitor.promclient.Gauge({ 80 | name: "my_gauge", 81 | help: "records my custom gauge metric", 82 | labelNames: [ "example_label" ] 83 | }); 84 | 85 | ... 86 | 87 | // and here we add a metric event that will be automatically exposed to /metrics endpoint 88 | myGauge.set({"example_label":"value"}, 220); 89 | ``` 90 | 91 | It is possible to capture error messages that were saved using `res.set("Error-Message", "foo message")`. For example: 92 | 93 | ```js 94 | res.set("Error-Message", "User not found"); 95 | ``` 96 | 97 | **Important**: This middleware requires to be put first in the middleware execution chain, so it can capture metrics from all possible requests. 98 | 99 | #### Manual request metrics collection 100 | 101 | Some cases you want to collect request times from requests not made by express endpoints in this cases you can use the `collectRequestTime` function. 102 | 103 | *Eg. Your app is subscribed to receive some request from a message broker and have to process it* 104 | 105 | ```js 106 | 107 | const start = process.hrtime(); 108 | 109 | try { 110 | internalProcessFunction(); 111 | Monitor.collectRequestTime("amqp", 200, 'queue_to_test', start); 112 | } catch (err) { 113 | Monitor.collectRequestTime('amqp', 500, 'queue_to_test', start, err); 114 | throw err; 115 | } 116 | ``` 117 | ## Dependency Metrics 118 | 119 | For you to know when a dependency is up or down, just provide a health check callback to be `watchDependencies` function: 120 | 121 | ```js 122 | const express = require("express"); 123 | const { Monitor } = require("@labbsr0x/express-monitor"); 124 | 125 | const app = express(); 126 | Monitor.init(app, true); 127 | 128 | // A RegisterDepedencyMetricsCallback will be automatically injected into the HealthCheckCallback 129 | Monitor.watchDependencies((register) => { 130 | // here you implement the logic to go after your dependencies and check their health 131 | register({ name: "Fake dependency 1", up: true}); 132 | register({ name: "Fake dependency 2", up: false}); 133 | }); 134 | ``` 135 | 136 | Now run your app and point prometheus to the defined metrics endpoint of your server. 137 | 138 | You also can monitor a dependency event. Just call `collectDependencyTime` and pass the right params. 139 | 140 | ```js 141 | Monitor.collectDependencyTime(name: string, type: string, statusCode: number, method: string, addr: string, errorMessage: string, start: [number, number]) 142 | ``` 143 | 144 | To properly collect the time of the dependency request, we suggest to define a variable to register the startTime right before calling the dependency, then pass it to the `collectDependencyTime` method: 145 | 146 | ```js 147 | const start = process.hrtime() 148 | try{ 149 | //using a service to create a slow request 150 | const response = await axios.get('http://slowwly.robertomurray.co.uk/delay/2000/url/http://google.com/') 151 | const { method, path } = response.request 152 | 153 | Monitor.collectDependencyTime("Google", "axios", response.status, method, path, "", start) 154 | ``` 155 | 156 | > :warning: **NOTE**: 157 | > If the errorMessage param is not provided, the isError label is automatically set to false. Thus, is required to pass a value in the errorMessage param, in order to set the isError to true. 158 | 159 | More details on how Prometheus works, you can find it [here](https://medium.com/@abilio.esteves/white-box-your-metrics-now-895a9e9d34ec). 160 | 161 | # Example 162 | 163 | In the `example` folder, you'll find a very simple but useful example to get you started. On your terminal, navigate to the project's root folder and type: 164 | 165 | ```bash 166 | npm i && tsc && npm i && cd example && npm i 167 | ``` 168 | 169 | and then 170 | 171 | ```bash 172 | npm start 173 | ``` 174 | 175 | On your browser, go to `localhost:3000` and then go to `localhost:3000/example/metrics` to see the exposed metrics. 176 | 177 | # Big Brother 178 | 179 | This is part of a more large application called [Big Brother](https://github.com/labbsr0x/big-brother). 180 | 181 | 182 | -------------------------------------------------------------------------------- /example/example.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const promClient = require("prom-client"); 3 | const axios = require("axios"); 4 | const { Monitor } = require("../dist/"); 5 | const { response } = require("express"); 6 | const app = express(); 7 | 8 | // inits the monitor with the express middleware to intercept the requests and register http metrics 9 | Monitor.init(app, true, [0.1, 1], "v1.0.0", (status) => { 10 | return (/^([345].+$).*/.exec(status)) != null // 3xx will also be considered as error 11 | }, "/example/metrics"); 12 | 13 | // inits a routine to expose health metrics 14 | Monitor.watchDependencies((register) => { 15 | register({ name: "Fake dependency 1", up: true}); 16 | register({ name: "Fake dependency 2", up: false}); 17 | }); 18 | 19 | // defines a custom metric 20 | var myGauge = new Monitor.promclient.Gauge({ 21 | name: "my_gauge", 22 | help: "records my custom gauge metric", 23 | labelNames: [ "example_label" ] 24 | }); 25 | 26 | // exposes an test api 27 | app.get("/", (req, res, next) => { 28 | const start = process.hrtime() 29 | myGauge.set({"example_label":"value"}, Math.random(100)); 30 | res.json({"ok": true}); 31 | Monitor.collectDependencyTime("dependencyNameTest", "fooType", 304, "GET", "/db", "Not Found", start) 32 | }) 33 | 34 | app.get("/axios", async (req, res) => { 35 | const start = process.hrtime() 36 | try{ 37 | //using a service to create a slow request 38 | const response = await axios.get('http://slowwly.robertomurray.co.uk/delay/2000/url/http://google.com/') 39 | const { method, path } = response.request 40 | 41 | Monitor.collectDependencyTime("Google", "axios", response.status, method, path, "", start) 42 | res.json({"ok": true}) 43 | }catch(err){ 44 | if (err.request) { 45 | const { method, path } = err.request._options 46 | Monitor.collectDependencyTime("Google", "axios", 404, method, path, "endpoint not found", start) 47 | res.json({"ok": false}) 48 | }else{ 49 | Monitor.collectDependencyTime("Google", "axios", 500, "GET", "/err", "server error", start) 50 | res.json({"ok": false}) 51 | } 52 | } 53 | }) 54 | 55 | // launches the service 56 | app.listen(3000, () => { 57 | console.log("Started!") 58 | }) -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "version": "1.0.0", 4 | "description": "Just a simple example to illustrate the use of the express-monitor lib", 5 | "main": "example.js", 6 | "scripts": { 7 | "start": "node example.js", 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/labbsr0x/express-monitor/example" 13 | }, 14 | "author": "", 15 | "license": "MIT", 16 | "dependencies": { 17 | "axios": "^0.20.0", 18 | "express": "^4.17.1" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Express Monitor 3 | * @module ExpressMonitor monitor 4 | */ 5 | 'use strict'; 6 | import monitor from "./lib/monitor" 7 | 8 | export const Monitor = monitor 9 | -------------------------------------------------------------------------------- /lib/monitor.ts: -------------------------------------------------------------------------------- 1 | import * as promclient from "prom-client"; 2 | import express from "express" 3 | 4 | let isCollectingMetrics = false; 5 | let dependencyRequestSeconds: promclient.Histogram; 6 | let reqSeconds: promclient.Histogram; 7 | 8 | export type Monitor = { 9 | init (app: express.Application, shouldCollectDefaultMetrics: boolean, buckets?: number[], version?: string, isErrorCallback?:isErrorCallback, metricsEndpoint?: string):void; 10 | promclient: typeof import("prom-client"); 11 | watchDependencies(healthCheckCallback: HealthCheckCallback):void; 12 | collectDependencyTime(name: string, type: string, statusCode: number, method: string, addr: string, errorMessage: string, start: [number, number]):void; 13 | collectRequestTime(type: string, statusCode: number, addr: string, start: [number, number], errorMessage?: string): void; 14 | getAddress(request: express.Request):string; 15 | }; 16 | 17 | export type HealthCheckResult = { 18 | name: string; 19 | up: boolean; 20 | }; 21 | 22 | export type isErrorCallback = (code:number|undefined) => boolean 23 | export type HealthCheckCallback = (callback: HealthCheckResultCallBack) => void 24 | export type HealthCheckResultCallBack = (result: HealthCheckResult) => void 25 | 26 | // a gauge to observe the dependency status 27 | const dependencyUp = new promclient.Gauge({ 28 | name: "dependency_up", 29 | help: "records if a dependency is up or down. 1 for up, 0 for down", 30 | labelNames: ["name"] 31 | }); 32 | 33 | const applicationInfo = new promclient.Gauge({ 34 | name: "application_info", 35 | help: "records static application info such as it's semantic version number", 36 | labelNames: ["version"] 37 | }); 38 | 39 | /** 40 | * Get http response content length in bytes 41 | */ 42 | function getContentLength(res: express.Response): number { 43 | let resContentLength = 0; 44 | if ("_contentLength" in res) { 45 | resContentLength = res['_contentLength']; 46 | } else { 47 | // Try header 48 | if (res.hasHeader('content-length')) { 49 | resContentLength = res.getHeader('content-length') as number; 50 | } 51 | } 52 | return resContentLength; 53 | } 54 | 55 | /** 56 | * Returns the diffence in seconds between a given start time and the current time 57 | * @param {[number,number]} start the start time of the dependecy request 58 | */ 59 | function diffTimeInSeconds(start: [number, number]) { 60 | const end = process.hrtime(start) 61 | const timeInSeconds = end[0] + (end[1] / 1000000000) 62 | return timeInSeconds 63 | } 64 | 65 | /** 66 | * @callback IsErrorCallback a callback to check if the status code is an error 67 | * @param {string} status the response status code 68 | * @returns {boolean} indicates weather the status code is considered an error or not 69 | */ 70 | 71 | /** 72 | * The default isError callback for HTTP status codes. Any status 4xx and 5xx are considered errors. Other are considered success. 73 | * @param {string} status the HTTP status code 74 | */ 75 | function defaultIsErrorCallback(status: number|undefined) { 76 | return (/^([45].+$).*/.exec(String(status))) != null 77 | } 78 | 79 | /** 80 | * Get error message from the response. If error message is null, sets the string to empty. 81 | * @param {HTTP response} res the http response 82 | * @returns a string with the error message or empty string if error message not found. 83 | */ 84 | function getErrorMessage(res: express.Response){ 85 | return res.get("Error-Message") ? res.get("Error-Message") : "" 86 | } 87 | 88 | /** 89 | * Returns the original registered route path, if requested URL has not been registered, It returns the originalURL without query string parameters. 90 | * @param {HTTP request} req the http request 91 | * @returns {string address} 92 | */ 93 | function getAddress(req: any) : string { 94 | if (typeof req.baseUrl === "undefined") { 95 | return req.originalUrl.split("?")[0]; 96 | } 97 | if (req.swagger) { 98 | return req.swagger.pathName 99 | } 100 | 101 | return req.baseUrl + (req.route && req.route.path ? req.route.path : ""); 102 | } 103 | 104 | /** 105 | * Collect latency metric dependency_request_seconds 106 | * @param {string} name the name of dependency 107 | * @param {string} type which request protocol was used (e.g. http, grpc, etc) 108 | * @param {number} statusCode the status code returned by the dependency request 109 | * @param {string} method the method of the dependency request (e.g GET, POST, PATH, DELETE) 110 | * @param {string} addr the path of the dependency request 111 | * @param {string} errormessage the error message of the dependency request 112 | * @param {[number,number]} start the start time of the dependecy request 113 | */ 114 | function collectDependencyTime(name: string, type: string, statusCode: number, method: string, addr: string, errorMessage: string, start: [number, number]){ 115 | const elapsedSeconds = diffTimeInSeconds(start) 116 | dependencyRequestSeconds.labels(name, type, String(statusCode), method, addr, String(!!errorMessage), errorMessage || '').observe(elapsedSeconds) 117 | } 118 | 119 | /** 120 | * Manual latency collect metric request_seconds 121 | * @param {string} type which request protocol was used (e.g. http, grpc, amqp, etc) 122 | * @param {number} statusCode the status code returned by the dependency request 123 | * @param {string} addr the path of the dependency request 124 | * @param {[number,number]} start the start time of the dependecy request 125 | * @param {string} errorMessage the error message of the dependency request 126 | */ 127 | function collectRequestTime(type: string, statusCode: number, addr: string, start: [number, number], errorMessage: string){ 128 | const elapsedSeconds = diffTimeInSeconds(start); 129 | reqSeconds.labels(type, String(statusCode), '', addr, String(!!errorMessage), errorMessage || '').observe(elapsedSeconds) 130 | } 131 | 132 | /** 133 | * Initializes the middleware for HTTP requests 134 | * @param {Express} app the express app 135 | * @param {?boolean} shouldCollectDefaultMetrics indicates weather we should expose default node js metrics 136 | * @param {?number[]} buckets configures the histogram buckets. if null or empty, defaults to [0.1, 0.3, 1.5, 10.5] 137 | * @param {?String} version your apps version 138 | * @param {?IsErrorCallback} isErrorCallback a callback function that determines which StatusCode are errors and which are not. Defaults to consider 4xx and 5xx errors 139 | * @param {?String} metricsEndpoint the endpoint where the metrics will be exposed. Defaults to /metrics. 140 | * @returns a PromClient to allow the addition of your custom metrics 141 | */ 142 | function init(app: express.Application, shouldCollectDefaultMetrics?: boolean, buckets?: number[], version?: string, isErrorCallback?: isErrorCallback, metricsEndpoint?: string) { 143 | if (!isCollectingMetrics && app) { 144 | if (typeof(isErrorCallback) !== "function") { 145 | isErrorCallback = defaultIsErrorCallback 146 | } 147 | 148 | if (!buckets || buckets.length === 0) { 149 | buckets = [0.1, 0.3, 1.5, 10.5] 150 | } 151 | 152 | // only inits once 153 | isCollectingMetrics = true; 154 | console.log("Init Prometheus monitoring"); 155 | 156 | // a cumulative histogram to collect http request metrics in well-defined buckets of interest 157 | reqSeconds = new promclient.Histogram({ 158 | name: "request_seconds", 159 | help: "records in a histogram the number of http requests and their duration in seconds", 160 | buckets: buckets, 161 | labelNames: ["type", "status", "method", "addr", "isError", "errorMessage"] 162 | }); 163 | 164 | // a counter to observe the response sizes 165 | const respSize = new promclient.Counter({ 166 | name: "response_size_bytes", 167 | help: "counts the size of each http response", 168 | labelNames: ["type", "status", "method", "addr", "isError", "errorMessage"] 169 | }); 170 | 171 | // a cumulative histogram to collect dependency request metrics in well-defined buckets of interest 172 | dependencyRequestSeconds = new promclient.Histogram({ 173 | name: "dependency_request_seconds", 174 | help: "records in a histogram the number of requests of a dependency and their duration in seconds", 175 | buckets: buckets, 176 | labelNames: ["name", "type", "status", "method", "addr", "isError", "errorMessage"] 177 | }) 178 | 179 | // middleware to capture prometheus metrics for the request 180 | app.all(/^(?!\/metrics$).*/, (req: express.Request, res: express.Response, next: express.NextFunction) => { 181 | let end = reqSeconds.startTimer() 182 | next(); 183 | res.once("finish", () => { 184 | const isErr = typeof isErrorCallback === "function" ? isErrorCallback(res.statusCode) : false; 185 | const errorMsg = getErrorMessage(res); 186 | const address = getAddress(req) 187 | 188 | // observes the request duration 189 | end({ 190 | "type": "http", 191 | "status": res.statusCode, 192 | "method": req.method, 193 | "addr": address, 194 | "isError": String(isErr), 195 | "errorMessage": errorMsg 196 | }) 197 | 198 | // observes the request response size 199 | respSize.inc({ 200 | "type": "http", 201 | "status": res.statusCode, 202 | "method": req.method, 203 | "addr": address, 204 | "isError": String(isErr), 205 | "errorMessage": errorMsg 206 | }, getContentLength(res)) 207 | }); 208 | }) 209 | 210 | metricsEndpoint = (!metricsEndpoint) ? "/metrics" : metricsEndpoint; // endpoint to collect all the registered metrics 211 | app.get(metricsEndpoint, (req, res) => { 212 | res.status(200).header("Content-Type", "text/plain").send(promclient.register.metrics()) 213 | }); 214 | 215 | if (shouldCollectDefaultMetrics) { 216 | // Probe system metrics every 5th second. 217 | promclient.collectDefaultMetrics({timeout: 5000}); 218 | } 219 | if(version){ 220 | applicationInfo.set({"version": version}, 1); 221 | } 222 | } 223 | } 224 | 225 | /** 226 | * @typedef {Object} HealthCheckResult structure to hold the health check result. Has a name (string) and an up (boolean) property 227 | * @property {string} name the name of the dependency 228 | * @property {boolean} up the status of the dependency. true for up, false for down 229 | */ 230 | 231 | /** 232 | * @callback RegisterDependencyMetricsCallback a callback to register the metrics for a specific dependency 233 | * @param {HealthCheckResult} result the result of health checking a specific dependency 234 | */ 235 | 236 | /** 237 | * @callback HealthCheckCallback a callback to check the health of the apps dependencies 238 | * @param {RegisterDependencyMetricsCallback} register a callback to register the metrics for a specific dependency 239 | * @returns {HealthCheckResult[]} an array of health check results, one for each dependency 240 | */ 241 | 242 | /** 243 | * Inits a routine to periodically watch the health of the app's dependencies. 244 | * Needs to return a valid array of HealthCheckResult. 245 | * @param {HealthCheckCallback} healthCheck 246 | */ 247 | function watchDependencies(healthCheck: HealthCheckCallback) { 248 | if (typeof healthCheck === 'function') { 249 | 250 | setInterval(() => { 251 | healthCheck(registerDependencyMetrics); 252 | }, 15000); 253 | 254 | } else { 255 | console.log("[Express Monitor][Watch Dependencies]: healthCheck callback needs to be a valid function") 256 | } 257 | } 258 | 259 | /** 260 | * Registers the current metrics for a specific dependency 261 | * @param {HealthCheckResult} result the result of health checking a specific dependency 262 | */ 263 | function registerDependencyMetrics(result: HealthCheckResult): void { 264 | if (result) { 265 | dependencyUp.set({"name": result.name}, (result.up ? 1 : 0)); 266 | } 267 | } 268 | const m: Monitor = { 269 | init, 270 | promclient, 271 | watchDependencies, 272 | collectDependencyTime, 273 | collectRequestTime, 274 | getAddress 275 | }; 276 | 277 | export default m 278 | 279 | 280 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@labbsr0x/express-monitor", 3 | "version": "2.11.0", 4 | "description": "A Prometheus middleware to add basic but very useful metrics for your Express JS app.", 5 | "main": "dist/index.js", 6 | "types": "dist/index.d.ts", 7 | "directories": { 8 | "lib": "lib" 9 | }, 10 | "scripts": { 11 | "build": "tsc", 12 | "prepublish": "tsc", 13 | "test": "mocha" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/labbsr0x/express-monitor.git" 18 | }, 19 | "keywords": [ 20 | "expressjs", 21 | "prometheus", 22 | "metrics" 23 | ], 24 | "author": "Labbsr0x", 25 | "license": "MIT", 26 | "bugs": { 27 | "url": "https://github.com/labbsr0x/express-monitor/issues" 28 | }, 29 | "homepage": "https://github.com/labbsr0x/express-monitor#readme", 30 | "dependencies": { 31 | "prom-client": "^11.5.3" 32 | }, 33 | "devDependencies": { 34 | "@types/express": "^4.17.7", 35 | "axios": "^0.20.0", 36 | "chai": "^4.2.0", 37 | "chai-http": "^4.3.0", 38 | "express": "^4.17.1", 39 | "mocha": "^8.0.1", 40 | "swagger-express-middleware": "^4.0.2", 41 | "typescript": "^3.9.7" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /test/app_test.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | const { Monitor } = require('../dist/') 3 | const bodyParser = require('body-parser'); 4 | const path = require('path'); 5 | const createMiddleware = require('swagger-express-middleware'); 6 | 7 | const app = express() 8 | 9 | Monitor.init(app, true) 10 | 11 | app.get('/test', (req, res) => { 12 | res.status(200) 13 | res.send('test') 14 | }) 15 | 16 | app.get('/testWithErrorMessage', (req, res) => { 17 | res.status(400) 18 | res.set("Error-Message", "Test Error Message") 19 | res.send("test error message") 20 | }) 21 | 22 | app.post('/test', (req, res) => { 23 | res.status(200) 24 | res.send('test post') 25 | }) 26 | 27 | app.get('/testPathParameter/:parameter', (req, res) => { 28 | res.status(200) 29 | res.send('test post') 30 | }) 31 | app.post('/testPathParameter/:param1/:param2', (req, res) => { 32 | res.status(400) 33 | res.set("Error-Message", `Test Error Message: param1=${req.params.param1} , param2=${req.params.param2}`) 34 | res.send() 35 | }) 36 | 37 | const router = express.Router() 38 | 39 | router.get('/testRouter', (req, res) => { 40 | res.send("test router") 41 | }) 42 | 43 | router.get('/testPath/:parameter', (req, res) => { 44 | res.send("parameter " + req.params.parameter); 45 | }) 46 | 47 | router.post('/testPath/:parameter1/action/:parameter2', (req, res) => { 48 | res.status(400) 49 | res.set("Error-Message", `Test Error Message: param1=${req.params.parameter1} , param2=${req.params.parameter2}`) 50 | res.send("test 2 path parameters") 51 | }) 52 | 53 | app.use('/router', router) 54 | 55 | 56 | const swaggerFile = path.join(__dirname, 'swagger/api/swagger-config.yml'); 57 | createMiddleware(swaggerFile, app, (error, middleware) => { 58 | if (error) { 59 | console.error('Error on Swagger Express', error); 60 | } 61 | app.use( 62 | middleware.metadata(), 63 | middleware.CORS(), 64 | middleware.files(), 65 | middleware.parseRequest(), 66 | middleware.validateRequest() 67 | ); 68 | app.use(bodyParser.text()); 69 | app.use(bodyParser.json()); 70 | app.use(bodyParser.urlencoded({ extended: true })); 71 | app.use((req, res, next) => { 72 | const controllerName = req.swagger.path['x-swagger-router-controller']; 73 | const operationId = req.swagger.operation['operationId']; 74 | console.log(`Run ${controllerName}[${operationId}]`); 75 | 76 | const controller = require(`./swagger/api/${controllerName}`); 77 | controller[operationId](req, res); 78 | 79 | next(); 80 | }); 81 | console.log('Swagger Express done'); 82 | }); 83 | 84 | module.exports = app -------------------------------------------------------------------------------- /test/swagger/api/swagger-config.yml: -------------------------------------------------------------------------------- 1 | swagger: "2.0" 2 | info: 3 | description: "Sample project using Express Monitor and Express Swagger Middleware" 4 | version: "1.0.0" 5 | title: "Swagger Express Monitor" 6 | host: localhost:3003 7 | # basePath: /v1 8 | schemes: 9 | - "http" 10 | paths: 11 | /users/{userId}: 12 | x-swagger-router-controller: users 13 | get: 14 | description: "Returns all users" 15 | operationId: "getAllUsers" 16 | parameters: 17 | - in: path 18 | name: userId 19 | schema: 20 | type: integer 21 | required: true 22 | description: Numeric ID of the user to get 23 | produces: 24 | - "application/json" 25 | responses: 26 | "200": 27 | description: "successful operation" 28 | "404": 29 | description: "Not found" -------------------------------------------------------------------------------- /test/swagger/api/users.js: -------------------------------------------------------------------------------- 1 | const getAllUsers = (req, res) => { 2 | console.log('Get all users'); 3 | return res.status(200).json({ 4 | success: true, 5 | count: 2, 6 | users: [{ 7 | name: 'A' 8 | }, { 9 | name: 'B' 10 | }, { 11 | name: 'C' 12 | }] 13 | }); 14 | }; 15 | 16 | module.exports = { 17 | getAllUsers 18 | }; -------------------------------------------------------------------------------- /test/test_monitor.js: -------------------------------------------------------------------------------- 1 | const chai = require('chai') 2 | const chaiHttp = require('chai-http') 3 | const { Monitor } = require('../dist/') 4 | const app = require('./app_test') 5 | const axios = require('axios'); 6 | 7 | chai.use(chaiHttp) 8 | 9 | const expect = chai.expect 10 | 11 | chai.should() 12 | 13 | describe('Collect metrics middleware', () => { 14 | 15 | it('should collect metric from basic route - GET', () => { 16 | chai.request(app) 17 | .get('/test') 18 | .set('Content-Type', 'application/json') 19 | .send() 20 | .end((err) => { 21 | if(err) console.log(err) 22 | }) 23 | chai.request(app) 24 | .get('/metrics') 25 | .set('Content-Type', 'application/json') 26 | .send() 27 | .end((err, res) => { 28 | expect(res.text).to.include('request_seconds_bucket{le="0.1",type="http",status="200",method="GET",addr="/test",isError="false",errorMessage=""} 1') 29 | expect(res.text).to.include('request_seconds_sum{type="http",status="200",method="GET",addr="/test",isError="false",errorMessage=""}') 30 | expect(res.text).to.include('request_seconds_count{type="http",status="200",method="GET",addr="/test",isError="false",errorMessage=""} 1') 31 | expect(res.text).to.include('response_size_bytes{type="http",status="200",method="GET",addr="/test",isError="false",errorMessage=""}') 32 | }) 33 | }) 34 | 35 | it('should collect metric from basic route - POST', () => { 36 | chai.request(app) 37 | .post('/test') 38 | .set('Content-Type', 'application/json') 39 | .send() 40 | .end((err) => { 41 | if(err) console.log(err) 42 | }) 43 | chai.request(app) 44 | .get('/metrics') 45 | .set('Content-Type', 'application/json') 46 | .send() 47 | .end((err, res) => { 48 | expect(res.text).to.include('request_seconds_bucket{le="0.1",type="http",status="200",method="POST",addr="/test",isError="false",errorMessage=""} 1') 49 | expect(res.text).to.include('request_seconds_sum{type="http",status="200",method="POST",addr="/test",isError="false",errorMessage=""}') 50 | expect(res.text).to.include('request_seconds_count{type="http",status="200",method="POST",addr="/test",isError="false",errorMessage=""} 1') 51 | expect(res.text).to.include('response_size_bytes{type="http",status="200",method="POST",addr="/test",isError="false",errorMessage=""}') 52 | }) 53 | }) 54 | 55 | it('should collect metric from express.Router', () => { 56 | chai.request(app) 57 | .get('/router/testRouter') 58 | .set('Content-Type', 'application/json') 59 | .send() 60 | .end((err) => { 61 | if(err) console.log(err) 62 | }) 63 | chai.request(app) 64 | .get('/metrics') 65 | .set('Content-Type', 'application/json') 66 | .send() 67 | .end((err, res) => { 68 | expect(res.text).to.include('request_seconds_bucket{le="0.1",type="http",status="200",method="GET",addr="/router/testRouter",isError="false",errorMessage=""} 1') 69 | expect(res.text).to.include('request_seconds_sum{type="http",status="200",method="GET",addr="/router/testRouter",isError="false",errorMessage=""}') 70 | expect(res.text).to.include('request_seconds_count{type="http",status="200",method="GET",addr="/router/testRouter",isError="false",errorMessage=""} 1') 71 | expect(res.text).to.include('response_size_bytes{type="http",status="200",method="GET",addr="/router/testRouter",isError="false",errorMessage=""}') 72 | }) 73 | }) 74 | 75 | it('should collect metric ignoring query string', () => { 76 | chai.request(app) 77 | .get('/test?param') 78 | .set('Content-Type', 'application/json') 79 | .send() 80 | .end((err) => { 81 | if(err) console.log(err) 82 | }) 83 | chai.request(app) 84 | .get('/metrics') 85 | .set('Content-Type', 'application/json') 86 | .send() 87 | .end((err, res) => { 88 | expect(res.text).to.include('request_seconds_bucket{le="0.1",type="http",status="200",method="GET",addr="/test",isError="false",errorMessage=""} 2') 89 | expect(res.text).to.include('request_seconds_sum{type="http",status="200",method="GET",addr="/test",isError="false",errorMessage=""}') 90 | expect(res.text).to.include('request_seconds_count{type="http",status="200",method="GET",addr="/test",isError="false",errorMessage=""} 2') 91 | expect(res.text).to.include('response_size_bytes{type="http",status="200",method="GET",addr="/test",isError="false",errorMessage=""}') 92 | }) 93 | }) 94 | 95 | it('should collect metric with error message', () => { 96 | chai.request(app) 97 | .get('/testWithErrorMessage') 98 | .set('Content-Type', 'application/json') 99 | .send() 100 | .end((err) => { 101 | if(err) console.log(err) 102 | }) 103 | chai.request(app) 104 | .get('/metrics') 105 | .set('Content-Type', 'application/json') 106 | .send() 107 | .end((err, res) => { 108 | expect(res.text).to.include('request_seconds_bucket{le="0.1",type="http",status="400",method="GET",addr="/testWithErrorMessage",isError="true",errorMessage="Test Error Message"} 1') 109 | expect(res.text).to.include('request_seconds_sum{type="http",status="400",method="GET",addr="/testWithErrorMessage",isError="true",errorMessage="Test Error Message"}') 110 | expect(res.text).to.include('request_seconds_count{type="http",status="400",method="GET",addr="/testWithErrorMessage",isError="true",errorMessage="Test Error Message"} 1') 111 | expect(res.text).to.include('response_size_bytes{type="http",status="400",method="GET",addr="/testWithErrorMessage",isError="true",errorMessage="Test Error Message"}') 112 | }) 113 | }) 114 | 115 | it('should use the original registered path in addr label when registered with express.Router - success response', () => { 116 | chai.request(app) 117 | .get('/router/testPath/pathParameter') 118 | .set('Content-Type', 'application/json') 119 | .send() 120 | .end((err) => { 121 | if(err) console.log(err) 122 | }) 123 | chai.request(app) 124 | .get('/metrics') 125 | .set('Content-Type', 'application/json') 126 | .send() 127 | .end((err, res) => { 128 | expect(res.text).to.include('request_seconds_bucket{le="0.1",type="http",status="200",method="GET",addr="/router/testPath/:parameter",isError="false",errorMessage=""} 1') 129 | expect(res.text).to.include('request_seconds_sum{type="http",status="200",method="GET",addr="/router/testPath/:parameter",isError="false",errorMessage=""}') 130 | expect(res.text).to.include('request_seconds_count{type="http",status="200",method="GET",addr="/router/testPath/:parameter",isError="false",errorMessage=""} 1') 131 | expect(res.text).to.include('response_size_bytes{type="http",status="200",method="GET",addr="/router/testPath/:parameter",isError="false",errorMessage=""}') 132 | }) 133 | }) 134 | 135 | it('should use the original registered path in addr label when registered with express.Router - error response', () => { 136 | chai.request(app) 137 | .post('/router/testPath/pathParameter1/action/pathParameter2') 138 | .set('Content-Type', 'application/json') 139 | .send() 140 | .end((err) => { 141 | if(err) console.log(err) 142 | }) 143 | chai.request(app) 144 | .get('/metrics') 145 | .set('Content-Type', 'application/json') 146 | .send() 147 | .end((err, res) => { 148 | expect(res.text).to.include('request_seconds_bucket{le="0.1",type="http",status="400",method="POST",addr="/router/testPath/:parameter1/action/:parameter2",isError="true",errorMessage="Test Error Message: param1=pathParameter1 , param2=pathParameter2"} 1') 149 | expect(res.text).to.include('request_seconds_sum{type="http",status="400",method="POST",addr="/router/testPath/:parameter1/action/:parameter2",isError="true",errorMessage="Test Error Message: param1=pathParameter1 , param2=pathParameter2"}') 150 | expect(res.text).to.include('request_seconds_count{type="http",status="400",method="POST",addr="/router/testPath/:parameter1/action/:parameter2",isError="true",errorMessage="Test Error Message: param1=pathParameter1 , param2=pathParameter2"} 1') 151 | expect(res.text).to.include('response_size_bytes{type="http",status="400",method="POST",addr="/router/testPath/:parameter1/action/:parameter2",isError="true",errorMessage="Test Error Message: param1=pathParameter1 , param2=pathParameter2"}') 152 | }) 153 | }) 154 | 155 | it('should use the original registered path in addr label when registered with basic router - success response', () => { 156 | chai.request(app) 157 | .get('/testPathParameter/pathParameterValue') 158 | .set('Content-Type', 'application/json') 159 | .send() 160 | .end((err) => { 161 | if(err) console.log(err) 162 | }) 163 | chai.request(app) 164 | .get('/metrics') 165 | .set('Content-Type', 'application/json') 166 | .send() 167 | .end((err, res) => { 168 | expect(res.text).to.include('request_seconds_bucket{le="0.1",type="http",status="200",method="GET",addr="/testPathParameter/:parameter",isError="false",errorMessage=""} 1') 169 | expect(res.text).to.include('request_seconds_sum{type="http",status="200",method="GET",addr="/testPathParameter/:parameter",isError="false",errorMessage=""}') 170 | expect(res.text).to.include('request_seconds_count{type="http",status="200",method="GET",addr="/testPathParameter/:parameter",isError="false",errorMessage=""} 1') 171 | expect(res.text).to.include('response_size_bytes{type="http",status="200",method="GET",addr="/testPathParameter/:parameter",isError="false",errorMessage=""}') 172 | }) 173 | }) 174 | 175 | it('should use the original registered path in addr label when registered with basic router - error response', () => { 176 | chai.request(app) 177 | .post('/testPathParameter/pathParameter1/pathParameter2') 178 | .set('Content-Type', 'application/json') 179 | .send() 180 | .end((err) => { 181 | if(err) console.log(err) 182 | }) 183 | chai.request(app) 184 | .get('/metrics') 185 | .set('Content-Type', 'application/json') 186 | .send() 187 | .end((err, res) => { 188 | expect(res.text).to.include('request_seconds_bucket{le="0.1",type="http",status="400",method="POST",addr="/testPathParameter/:param1/:param2",isError="true",errorMessage="Test Error Message: param1=pathParameter1 , param2=pathParameter2"} 1') 189 | expect(res.text).to.include('request_seconds_sum{type="http",status="400",method="POST",addr="/testPathParameter/:param1/:param2",isError="true",errorMessage="Test Error Message: param1=pathParameter1 , param2=pathParameter2"}') 190 | expect(res.text).to.include('request_seconds_count{type="http",status="400",method="POST",addr="/testPathParameter/:param1/:param2",isError="true",errorMessage="Test Error Message: param1=pathParameter1 , param2=pathParameter2"} 1') 191 | expect(res.text).to.include('response_size_bytes{type="http",status="400",method="POST",addr="/testPathParameter/:param1/:param2",isError="true",errorMessage="Test Error Message: param1=pathParameter1 , param2=pathParameter2"}') 192 | }) 193 | }) 194 | 195 | it('should use the original requested URL in addr label and status 404 when there is no registered route for requested URL', async () => { 196 | chai.request(app) 197 | .post('/app/unregistered-path') 198 | .set('Content-Type', 'application/json') 199 | .send() 200 | .end((err) => { 201 | if(err) console.log(err) 202 | }) 203 | await chai.request(app) 204 | .get('/metrics') 205 | .set('Content-Type', 'application/json') 206 | .send() 207 | .end((err, res) => { 208 | expect(res.text).to.include('request_seconds_bucket{le="0.1",type="http",status="404",method="POST",addr="/app/unregistered-path",isError="true",errorMessage=""} 1') 209 | expect(res.text).to.include('request_seconds_sum{type="http",status="404",method="POST",addr="/app/unregistered-path",isError="true",errorMessage=""}') 210 | expect(res.text).to.include('request_seconds_count{type="http",status="404",method="POST",addr="/app/unregistered-path",isError="true",errorMessage=""} 1') 211 | expect(res.text).to.include('response_size_bytes{type="http",status="404",method="POST",addr="/app/unregistered-path",isError="true",errorMessage=""}') 212 | }) 213 | }) 214 | 215 | it('should use the original requested URL (without query string parameters) in addr label and status 404 when there is no registered route for requested URL', async () => { 216 | chai.request(app) 217 | .post('/unregistered-path-with-query-string?param=paramValue&name=Jhon&surname=Doe') 218 | .set('Content-Type', 'application/json') 219 | .send() 220 | .end((err) => { 221 | if(err) console.log(err) 222 | }) 223 | await chai.request(app) 224 | .get('/metrics') 225 | .set('Content-Type', 'application/json') 226 | .send() 227 | .end((err, res) => { 228 | expect(res.text).to.include('request_seconds_bucket{le="0.1",type="http",status="404",method="POST",addr="/unregistered-path-with-query-string",isError="true",errorMessage=""} 1') 229 | expect(res.text).to.include('request_seconds_sum{type="http",status="404",method="POST",addr="/unregistered-path-with-query-string",isError="true",errorMessage=""}') 230 | expect(res.text).to.include('request_seconds_count{type="http",status="404",method="POST",addr="/unregistered-path-with-query-string",isError="true",errorMessage=""} 1') 231 | expect(res.text).to.include('response_size_bytes{type="http",status="404",method="POST",addr="/unregistered-path-with-query-string",isError="true",errorMessage=""}') 232 | }) 233 | }) 234 | 235 | it('should collect depedency metric', async() => { 236 | const start = process.hrtime() 237 | const response = await axios.get('http://google.com/') 238 | const { method, path } = response.request 239 | Monitor.collectDependencyTime("Google", "axios", response.status, method, path, "", start) 240 | chai.request(app) 241 | .get('/metrics') 242 | .set('Content-Type', 'application/json') 243 | .send() 244 | .end((err, res) => { 245 | expect(res.text).to.include('dependency_request_seconds_bucket{le="0.1",name="Google",type="axios",status="200",method="GET",addr="/",isError="false",errorMessage=""}') 246 | expect(res.text).to.include('dependency_request_seconds_bucket{le="0.3",name="Google",type="axios",status="200",method="GET",addr="/",isError="false",errorMessage=""}') 247 | expect(res.text).to.include('dependency_request_seconds_bucket{le="1.5",name="Google",type="axios",status="200",method="GET",addr="/",isError="false",errorMessage=""}') 248 | expect(res.text).to.include('dependency_request_seconds_bucket{le="10.5",name="Google",type="axios",status="200",method="GET",addr="/",isError="false",errorMessage=""}') 249 | expect(res.text).to.include('dependency_request_seconds_bucket{le="+Inf",name="Google",type="axios",status="200",method="GET",addr="/",isError="false",errorMessage=""} 1') 250 | expect(res.text).to.include('dependency_request_seconds_sum{name="Google",type="axios",status="200",method="GET",addr="/",isError="false",errorMessage=""}') 251 | expect(res.text).to.include('dependency_request_seconds_count{name="Google",type="axios",status="200",method="GET",addr="/",isError="false",errorMessage=""} 1') 252 | }) 253 | }); 254 | 255 | it('should set isError to false when not contains error message - collectDependencyTime', async() => { 256 | const start = process.hrtime() 257 | Monitor.collectDependencyTime("dependencyNameTest", "fooType", 304, "GET", "/test", "", start) 258 | chai.request(app) 259 | .get('/metrics') 260 | .set('Content-Type', 'application/json') 261 | .send() 262 | .end((err, res) => { 263 | expect(res.text).to.include('dependency_request_seconds_bucket{le="0.1",name="dependencyNameTest",type="fooType",status="304",method="GET",addr="/test",isError="false",errorMessage=""}') 264 | expect(res.text).to.include('dependency_request_seconds_sum{name="dependencyNameTest",type="fooType",status="304",method="GET",addr="/test",isError="false",errorMessage=""}') 265 | expect(res.text).to.include('dependency_request_seconds_count{name="dependencyNameTest",type="fooType",status="304",method="GET",addr="/test",isError="false",errorMessage=""} 1') 266 | }) 267 | }) 268 | 269 | it('should set isError to true when contains error message - collectDependencyTime', async() => { 270 | const start = process.hrtime() 271 | const errorMessage = "Foo error" 272 | Monitor.collectDependencyTime("dependencyNameTest", "fooType", 304, "GET", "/test", errorMessage, start) 273 | chai.request(app) 274 | .get('/metrics') 275 | .set('Content-Type', 'application/json') 276 | .send() 277 | .end((err, res) => { 278 | expect(res.text).to.include('dependency_request_seconds_bucket{le="0.1",name="dependencyNameTest",type="fooType",status="304",method="GET",addr="/test",isError="true",errorMessage="Foo error"}') 279 | expect(res.text).to.include('dependency_request_seconds_sum{name="dependencyNameTest",type="fooType",status="304",method="GET",addr="/test",isError="true",errorMessage="Foo error"}') 280 | expect(res.text).to.include('dependency_request_seconds_count{name="dependencyNameTest",type="fooType",status="304",method="GET",addr="/test",isError="true",errorMessage="Foo error"} 1') 281 | }) 282 | }) 283 | 284 | it('should collect non express request', async () => { 285 | const start = process.hrtime(); 286 | 287 | const fakeInternalProcess = await new Promise(resolve => setTimeout(resolve, 50)); 288 | Monitor.collectRequestTime('amqp', 200, 'queue_to_test', start); 289 | 290 | chai.request(app) 291 | .get('/metrics') 292 | .set('Content-Type', 'application/json') 293 | .send() 294 | .end((err, res) => { 295 | expect(res.text).to.include('request_seconds_bucket{le="0.1",type="amqp",status="200",method="",addr="queue_to_test",isError="false",errorMessage=""} 1'); 296 | expect(res.text).to.include('request_seconds_sum{type="amqp",status="200",method="",addr="queue_to_test",isError="false",errorMessage=""}'); 297 | expect(res.text).to.include('request_seconds_count{type="amqp",status="200",method="",addr="queue_to_test",isError="false",errorMessage=""} 1'); 298 | }) 299 | }); 300 | 301 | it('should collect non express request with error', async () => { 302 | const start = process.hrtime(); 303 | 304 | try{ 305 | await new Promise((_, reject) => setTimeout(() => reject('dummy error'), 30)); 306 | }catch (err) { 307 | Monitor.collectRequestTime('amqp', 500, 'queue_to_test', start, err) 308 | } 309 | 310 | chai.request(app) 311 | .get('/metrics') 312 | .set('Content-Type', 'application/json') 313 | .send() 314 | .end((err, res) => { 315 | expect(res.text).to.include('request_seconds_bucket{le="0.1",type="amqp",status="500",method="",addr="queue_to_test",isError="true",errorMessage="dummy error"} 1'); 316 | expect(res.text).to.include('request_seconds_sum{type="amqp",status="500",method="",addr="queue_to_test",isError="true",errorMessage="dummy error"}'); 317 | expect(res.text).to.include('request_seconds_count{type="amqp",status="500",method="",addr="queue_to_test",isError="true",errorMessage="dummy error"} 1'); 318 | }) 319 | }) 320 | 321 | it('should collect swagger-express-middleware metrics', () => { 322 | chai.request(app) 323 | .get('/users/123') 324 | .set('Content-Type', 'application/json') 325 | .send() 326 | .end((err) => { 327 | if(err) console.log(err) 328 | }) 329 | chai.request(app) 330 | .get('/metrics') 331 | .set('Content-Type', 'application/json') 332 | .send() 333 | .end((err, res) => { 334 | expect(res.text).to.include('request_seconds_bucket{le="0.1",type="http",status="200",method="GET",addr="/users/{userId}",isError="false",errorMessage=""} 1') 335 | expect(res.text).to.include('request_seconds_sum{type="http",status="200",method="GET",addr="/users/{userId}",isError="false",errorMessage=""}') 336 | expect(res.text).to.include('request_seconds_count{type="http",status="200",method="GET",addr="/users/{userId}",isError="false",errorMessage=""} 1') 337 | expect(res.text).to.include('response_size_bytes{type="http",status="200",method="GET",addr="/users/{userId}",isError="false",errorMessage=""}') 338 | }) 339 | }) 340 | }); -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "ES2020", 5 | "noImplicitAny": true, 6 | "removeComments": true, 7 | "preserveConstEnums": true, 8 | "esModuleInterop": true, 9 | "strict": true, 10 | "declaration": true, 11 | "outDir": "dist" 12 | }, 13 | "exclude": [ 14 | "node_modules", 15 | "dist" 16 | ] 17 | } --------------------------------------------------------------------------------