├── .eslintrc.json ├── .github └── workflows │ ├── main.yml │ └── release.yml ├── .gitignore ├── .npmignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── src ├── express-middleware.js ├── index.d.ts ├── index.js ├── koa-middleware.js ├── metrics-middleware.js ├── request-response-collector.js └── utils.js ├── test ├── integration-tests │ ├── express │ │ ├── middleware-exclude-routes-test.js │ │ ├── middleware-test.js │ │ ├── middleware-unique-metrics-name-test.js │ │ └── server │ │ │ ├── config.js │ │ │ ├── express-server-exclude-routes.js │ │ │ ├── express-server.js │ │ │ ├── package.json │ │ │ ├── router.js │ │ │ └── sub-router.js │ ├── koa │ │ ├── middleware-exclude-routes-test.js │ │ ├── middleware-test.js │ │ ├── middleware-unique-metrics-name-test.js │ │ └── server │ │ │ ├── config.js │ │ │ ├── koa-server-exclude-routes.js │ │ │ ├── koa-server.js │ │ │ ├── package.json │ │ │ ├── router.js │ │ │ └── sub-router.js │ └── nest-js │ │ ├── middleware-test.spec.ts │ │ ├── users.controller.ts │ │ └── users.module.ts ├── unit-test │ ├── metric-middleware-koa-test.js │ ├── metric-middleware-test.js │ ├── request-response-collector-test.js │ └── utils-test.js └── utils │ └── sleep.js └── tsconfig.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "standard", 3 | "plugins": [ 4 | "standard", 5 | "promise", 6 | "chai-friendly" 7 | ], 8 | "env": { 9 | "node": true, 10 | "mocha": true 11 | }, 12 | "rules": { 13 | "eol-last": "off", 14 | "space-before-function-paren": "off", 15 | "indent": ["error", 4], 16 | "quotes": ["error", "single", { "avoidEscape": true }], 17 | "semi": ["error", "always",{ "omitLastInOneLineBlock": true}], 18 | "one-var": ["off"], 19 | "space-before-blocks": ["off"], 20 | "camelcase": ["warn"], 21 | "no-unused-expressions": 0, 22 | "chai-friendly/no-unused-expressions": 2, 23 | "no-prototype-builtins": "off" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [ push, pull_request ] 4 | 5 | env: 6 | NODE_VERSION: 22 7 | 8 | jobs: 9 | lockfile-lint: 10 | name: Lockfile lint 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | - uses: actions/setup-node@v4 15 | with: 16 | node-version: ${{ env.NODE_VERSION }} 17 | - name: lint lock file 18 | run: npx lockfile-lint --path package-lock.json --allowed-hosts npm --validate-https 19 | 20 | quality-checks: 21 | needs: [ 'lockfile-lint' ] 22 | name: Quality checks 23 | runs-on: ubuntu-latest 24 | steps: 25 | - uses: actions/checkout@v4 26 | - uses: actions/setup-node@v4 27 | with: 28 | node-version: ${{ env.NODE_VERSION }} 29 | - name: install dependencies 30 | run: npm ci --ignore-scripts --no-audit --no-funds 31 | - name: types check 32 | run: npm run types-check 33 | - name: lint check 34 | run: npm run lint 35 | 36 | test: 37 | strategy: 38 | matrix: 39 | NODE_VERSION: [ '18', '20', '22' ] 40 | needs: [ 'lockfile-lint' ] 41 | name: Node v.${{ matrix.NODE_VERSION }} Tests 42 | runs-on: ubuntu-latest 43 | steps: 44 | - uses: actions/checkout@v4 45 | - uses: actions/setup-node@v4 46 | with: 47 | node-version: ${{ matrix.NODE_VERSION }} 48 | - uses: actions/cache@v4 49 | with: 50 | path: '**/node_modules' 51 | key: ${{ matrix.NODE_VERSION }}-node-${{ hashFiles('**/package-lock.json') }} 52 | - name: install dependencies 53 | if: steps.cache.outputs.cache-hit != 'true' 54 | run: npm ci --ignore-scripts --no-audit --no-funds 55 | - name: unit tests 56 | run: npm run unit-tests 57 | - name: integration tests 58 | run: npm run integration-tests 59 | - name: coveralls 60 | uses: coverallsapp/github-action@master 61 | if: ${{ matrix.NODE_VERSION == env.NODE_VERSION }} 62 | with: 63 | github-token: ${{ secrets.GITHUB_TOKEN }} 64 | 65 | prom-client-test: 66 | needs: [ 'lockfile-lint' ] 67 | strategy: 68 | matrix: 69 | prom-client: [ '12', '13', '14', '15' ] 70 | name: Prom Client v.${{ matrix.prom-client }} Tests (node ${{ matrix.NODE_VERSION }}) 71 | runs-on: ubuntu-latest 72 | steps: 73 | - uses: actions/checkout@v4 74 | - uses: actions/setup-node@v4 75 | with: 76 | node-version: ${{ env.NODE_VERSION }} 77 | - name: install dependencies 78 | run: npm ci --ignore-scripts --no-audit --no-funds 79 | - name: install prom client 80 | run: npm i prom-client@${{ matrix.prom-client }} 81 | - name: run tests 82 | run: npm test 83 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: [ workflow_dispatch ] 4 | 5 | jobs: 6 | release: 7 | permissions: 8 | actions: write 9 | checks: write 10 | contents: write 11 | deployments: write 12 | issues: write 13 | packages: write 14 | pull-requests: write 15 | repository-projects: write 16 | security-events: write 17 | statuses: write 18 | name: release 19 | runs-on: ubuntu-latest 20 | if: github.ref == 'refs/heads/master' 21 | steps: 22 | - uses: actions/checkout@v4 23 | - uses: actions/setup-node@v4 24 | with: 25 | node-version: '22' 26 | - name: install dependencies 27 | run: npm ci --ignore-scripts --no-audit --no-funds 28 | - name: release 29 | run: npx semantic-release 30 | env: 31 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 32 | NPM_TOKEN: ${{secrets.NPM_TOKEN}} 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .history/ 3 | .vscode/ 4 | coverage/ 5 | .nyc_output/ 6 | .idea/* 7 | .stryker-tmp* -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | test/* 2 | node_modules 3 | .history/ 4 | .vscode/ 5 | coverage/ 6 | .nyc_output/ 7 | .idea/* 8 | .stryker-tmp/* 9 | tsconfig.json 10 | .travis.yml 11 | .eslintrc.json 12 | LICENSE -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## Master 4 | 5 | ### Features 6 | 7 | - Add support for custom labels addition to metrics 8 | 9 | ### Improvements 10 | 11 | - Add support for `prom-client v13`, which includes a few breaking changes, mainly the following functions are now async (return a promise): 12 | ``` 13 | registry.metrics() 14 | registry.getMetricsAsJSON() 15 | registry.getMetricsAsArray() 16 | registry.getSingleMetricAsString() 17 | ``` 18 | More info at [`prom-client v13` Release Page](https://github.com/siimon/prom-client/releases/tag/v13.0.0). 19 | 20 | ## 3.1.0 - 3 September, 2020 21 | 22 | - Added support for axios responses while using axios-time plugin 23 | 24 | ## 3.0.0 - 2 September, 2020 25 | 26 | ### Breaking changes 27 | 28 | - Drop Node 6/8 support 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Prometheus API Monitoring 2 | 3 | [![NPM Version][npm-image]][npm-url] 4 | [![NPM Downloads][downloads-image]][downloads-url] 5 | [![Build Status][travis-image]][travis-url] 6 | [![Test Coverage][coveralls-image]][coveralls-url] 7 | [![Known Vulnerabilities][snyk-image]][snyk-url] 8 | [![Apache 2.0 License][license-image]][license-url] 9 | 10 | 11 | 12 | 13 | 14 | 15 | - [Goal](#goal) 16 | - [Features](#features) 17 | - [Usage](#usage) 18 | - [Options](#options) 19 | - [Access the metrics](#access-the-metrics) 20 | - [Custom Metrics](#custom-metrics) 21 | - [Note](#note) 22 | - [Additional Metric Labels](#additional-metric-labels) 23 | - [Request.js HTTP request duration collector](#requestjs-http-request-duration-collector) 24 | - [Usage](#usage-1) 25 | - [Initialize](#initialize) 26 | - [Options](#options-1) 27 | - [request](#request) 28 | - [request-promise-native](#request-promise-native) 29 | - [axios](#axios) 30 | - [Usage in koa](#usage-in-koa) 31 | - [Test](#test) 32 | - [Prometheus Examples Queries](#prometheus-examples-queries) 33 | - [Apdex](#apdex) 34 | - [95th Response Time by specific route and status code](#95th-response-time-by-specific-route-and-status-code) 35 | - [Median Response Time Overall](#median-response-time-overall) 36 | - [Median Request Size Overall](#median-request-size-overall) 37 | - [Median Response Size Overall](#median-response-size-overall) 38 | - [Average Memory Usage - All services](#average-memory-usage---all-services) 39 | - [Average Eventloop Latency - All services](#average-eventloop-latency---all-services) 40 | 41 | 42 | 43 | ## Goal 44 | 45 | API and process monitoring with [Prometheus](https://prometheus.io) for Node.js micro-service 46 | 47 | **Note: Prometheus (`prom-client`) is a peer dependency since 1.x version** 48 | 49 | ## Features 50 | 51 | - [Collect API metrics for each call](#usage) 52 | - Response time in seconds 53 | - Request size in bytes 54 | - Response size in bytes 55 | - Add prefix to metrics names - custom or project name 56 | - Exclude specific routes from being collect 57 | - Number of open connections to the server 58 | - Process Metrics as recommended by Prometheus [itself](https://prometheus.io/docs/instrumenting/writing_clientlibs/#standard-and-runtime-collectors) 59 | - Endpoint to retrieve the metrics - used for Prometheus scraping 60 | - Prometheus format 61 | - JSON format (`${path}.json`) 62 | - Support custom metrics 63 | - [Http function to collect request.js HTTP request duration](#requestjs-http-request-duration-collector) 64 | 65 | ## Usage 66 | 67 | ```js 68 | const apiMetrics = require('prometheus-api-metrics'); 69 | app.use(apiMetrics()) 70 | ``` 71 | 72 | ### Options 73 | 74 | | Option | Type | Description | Default Value | 75 | |--------------------------|-----------|-------------|---------------| 76 | | `metricsPath` | `String` | Path to access the metrics | `/metrics` | 77 | | `defaultMetricsInterval` | `Number` | Interval to collect the process metrics in milliseconds | `10000` | 78 | | `durationBuckets` | `Array` | Buckets for response time in seconds | `[0.001, 0.005, 0.015, 0.05, 0.1, 0.2, 0.3, 0.4, 0.5]` | 79 | | `requestSizeBuckets` | `Array` | Buckets for request size in bytes | `[5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000, 10000]` | 80 | | `responseSizeBuckets` | `Array` | Buckets for response size in bytes | `[5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000, 10000]` | 81 | | `useUniqueHistogramName` | `Boolean` | Add to metrics names the project name as a prefix (from package.json) | `false` | 82 | | `metricsPrefix` | `String` | A custom metrics names prefix, the package will add underscore between your prefix to the metric name | | 83 | | `excludeRoutes` | `Array` | Array of routes to exclude. Routes should be in your framework syntax | | 84 | | `includeQueryParams` | `Boolean` | Indicate if to include query params in route, the query parameters will be sorted in order to eliminate the number of unique labels | `false` | 85 | | `additionalLabels` | `Array` | Indicating custom labels that can be included on each `http_*` metric. Use in conjunction with `extractAdditionalLabelValuesFn`. | 86 | | `extractAdditionalLabelValuesFn` | `Function` | A function that can be use to generate the value of custom labels for each of the `http_*` metrics. When using koa, the function takes `ctx`, when using express, it takes `req, res` as arguments | | 87 | 88 | ### Access the metrics 89 | 90 | To get the metrics in Prometheus format use: 91 | 92 | ```sh 93 | curl http[s]://:[port]/metrics 94 | ``` 95 | 96 | To get the metrics in JSON format use: 97 | 98 | ```sh 99 | curl http[s]://:[port]/metrics.json 100 | ``` 101 | 102 | **Note:** 103 | 104 | 1. If you pass to the middleware the `metricsPath` option the path will be the one that you chose. 105 | 106 | 2. If you are using express framework and no route was found for the request (e.g: 404 status code), the request will not be collected. that's because we'll risk memory leak since the route is not a pattern but a hardcoded string. 107 | 108 | ## Custom Metrics 109 | 110 | You can expand the API metrics with more metrics that you would like to expose. 111 | All you have to do is: 112 | 113 | Require prometheus client 114 | 115 | ```js 116 | const Prometheus = require('prom-client'); 117 | ``` 118 | 119 | Create new metric from the kind that you like 120 | 121 | ```js 122 | const checkoutsTotal = new Prometheus.Counter({ 123 | name: 'checkouts_total', 124 | help: 'Total number of checkouts', 125 | labelNames: ['payment_method'] 126 | }); 127 | ``` 128 | 129 | Update it: 130 | 131 | ```js 132 | checkoutsTotal.inc({ 133 | payment_method: paymentMethod 134 | }) 135 | ``` 136 | 137 | The custom metrics will be exposed under the same endpoint as the API metrics. 138 | 139 | For more info about the Node.js Prometheus client you can read [here](https://github.com/siimon/prom-client#prometheus-client-for-nodejs--) 140 | 141 | ### Note 142 | 143 | This will work only if you use the default Prometheus registry - do not use `new Prometheus.Registry()` 144 | 145 | ## Additional Metric Labels 146 | 147 | You can define additional metric labels by using `additionalLabels` and `extractAdditionalLabelValuesFn` options. 148 | 149 | For instance: 150 | 151 | ```js 152 | const apiMetrics = require('prometheus-api-metrics'); 153 | app.use(apiMetrics({ 154 | additionalLabels: ['customer', 'cluster'], 155 | extractAdditionalLabelValuesFn: (req, res) => { 156 | const { headers } = req.headers; 157 | return { 158 | customer: headers['x-custom-header-customer'], 159 | cluster: headers['x-custom-header-cluster'] 160 | } 161 | } 162 | })) 163 | ``` 164 | 165 | ## Request.js HTTP request duration collector 166 | 167 | This feature enables you to easily process the result of Request.js timings feature. 168 | 169 | ### Usage 170 | 171 | #### Initialize 172 | 173 | You can choose to initialized this functionality as a Class or not 174 | 175 | **Class:** 176 | 177 | ```js 178 | const HttpMetricsCollector = require('prometheus-api-metrics').HttpMetricsCollector; 179 | const collector = new HttpMetricsCollector(); 180 | collector.init(); 181 | ``` 182 | 183 | **Singleton:** 184 | 185 | ```js 186 | const HttpMetricsCollector = require('prometheus-api-metrics').HttpMetricsCollector; 187 | HttpMetricsCollector.init(); 188 | ``` 189 | 190 | #### Options 191 | 192 | - durationBuckets - the histogram buckets for request duration. 193 | - countClientErrors - Boolean that indicates whether to collect client errors as Counter, this counter will have target and error code labels. 194 | - useUniqueHistogramName - Add to metrics names the project name as a prefix (from package.json) 195 | - prefix - A custom metrics names prefix, the package will add underscore between your prefix to the metric name. 196 | 197 | For Example: 198 | 199 | #### request 200 | 201 | ```js 202 | request({ url: 'http://www.google.com', time: true }, (err, response) => { 203 | Collector.collect(err || response); 204 | }); 205 | ``` 206 | 207 | #### request-promise-native 208 | 209 | ```js 210 | return requestPromise({ method: 'POST', url: 'http://www.mocky.io/v2/5bd9984b2f00006d0006d1fd', route: 'v2/:id', time: true, resolveWithFullResponse: true }).then((response) => { 211 | Collector.collect(response); 212 | }).catch((error) => { 213 | Collector.collect(error); 214 | }); 215 | ``` 216 | 217 | **Notes:** 218 | 219 | 1. In order to use this feature you must use `{ time: true }` as part of your request configuration and then pass to the collector the response or error you got. 220 | 2. In order to use the timing feature in request-promise/request-promise-native you must also use `resolveWithFullResponse: true` 221 | 3. Override - you can override the `route` and `target` attribute instead of taking them from the request object. In order to do that you should set a `metrics` object on your request with those attribute: 222 | ```js 223 | request({ method: 'POST', url: 'http://www.mocky.io/v2/5bd9984b2f00006d0006d1fd', metrics: { target: 'www.google.com', route: 'v2/:id' }, time: true }, (err, response) => {...}; 224 | }); 225 | ``` 226 | 227 | #### axios 228 | 229 | ```js 230 | const axios = require('axios'); 231 | const axiosTime = require('axios-time'); 232 | 233 | axiosTime(axios); 234 | 235 | try { 236 | const response = await axios({ baseURL: 'http://www.google.com', method: 'get', url: '/' }); 237 | Collector.collect(response); 238 | } catch (error) { 239 | Collector.collect(error); 240 | } 241 | ``` 242 | 243 | **Notes:** 244 | 245 | - In order to collect metrics from axios client the [`axios-time`](https://www.npmjs.com/package/axios-time) package is required. 246 | 247 | ## Usage in koa 248 | 249 | This package supports koa server that uses [`koa-router`](https://www.npmjs.com/package/koa-router) and [`koa-bodyparser`](https://www.npmjs.com/package/koa-bodyparser) 250 | 251 | ```js 252 | const { koaMiddleware } = require('prometheus-api-metrics') 253 | 254 | app.use(koaMiddleware()) 255 | ``` 256 | 257 | ## Test 258 | 259 | ```sh 260 | npm test 261 | ``` 262 | 263 | ## Prometheus Examples Queries 264 | 265 | ### [Apdex](https://en.wikipedia.org/wiki/Apdex) 266 | 267 | ``` 268 | (sum(rate(http_request_duration_seconds_bucket{="">, route="", le="0.05"}[10m])) by () + sum(rate(http_request_duration_seconds_bucket{="", route="", le="0.1"}[10m])) by ()) / 2 / sum(rate(http_request_duration_seconds_count{="", route=""}[10m])) by () 269 | ``` 270 | 271 | ### 95th Response Time by specific route and status code 272 | 273 | ``` 274 | histogram_quantile(0.95, sum(rate(http_request_duration_seconds_bucket{="", route="", code="200"}[10m])) by (le)) 275 | ``` 276 | 277 | ### Median Response Time Overall 278 | 279 | ``` 280 | histogram_quantile(0.50, sum(rate(http_request_duration_seconds_bucket{=""}[10m])) by (le)) 281 | ``` 282 | 283 | ### Median Request Size Overall 284 | 285 | ``` 286 | histogram_quantile(0.50, sum(rate(http_request_size_bytes_bucket{=""}[10m])) by (le)) 287 | ``` 288 | 289 | ### Median Response Size Overall 290 | 291 | ``` 292 | histogram_quantile(0.50, sum(rate(http_response_size_bytes_bucket{=""}[10m])) by (le)) 293 | ``` 294 | 295 | ### Average Memory Usage - All services 296 | 297 | ``` 298 | avg(nodejs_external_memory_bytes / 1024 / 1024) by (=12 <=16" 42 | }, 43 | "devDependencies": { 44 | "@nestjs/common": "^7.6.18", 45 | "@nestjs/core": "^7.6.18", 46 | "@nestjs/platform-express": "^7.6.18", 47 | "@nestjs/testing": "^7.6.18", 48 | "@types/mocha": "^10.0.10", 49 | "axios": "^0.21.4", 50 | "axios-time": "^1.0.0", 51 | "body-parser": "^1.18.3", 52 | "chai": "^4.5.0", 53 | "chai-as-promised": "^7.1.2", 54 | "coveralls": "^3.1.1", 55 | "doctoc": "^1.4.0", 56 | "eslint": "^6.8.0", 57 | "eslint-config-standard": "^14.1.1", 58 | "eslint-plugin-chai-friendly": "^0.5.0", 59 | "eslint-plugin-import": "^2.31.0", 60 | "eslint-plugin-mocha": "^6.3.0", 61 | "eslint-plugin-node": "^11.1.0", 62 | "eslint-plugin-promise": "^4.3.1", 63 | "eslint-plugin-standard": "^4.1.0", 64 | "express": "^4.21.2", 65 | "koa": "^2.16.0", 66 | "koa-bodyparser": "^4.4.1", 67 | "koa-router": "^7.4.0", 68 | "lodash.clonedeep": "^4.5.0", 69 | "mocha": "^8.4.0", 70 | "nock": "^10.0.6", 71 | "node-mocks-http": "^1.16.2", 72 | "nyc": "^15.1.0", 73 | "prom-client": "^13.2.0", 74 | "reflect-metadata": "^0.1.14", 75 | "request": "^2.88.2", 76 | "request-promise-native": "^1.0.9", 77 | "rewire": "^4.0.1", 78 | "rxjs": "^6.6.7", 79 | "sinon": "^5.1.1", 80 | "supertest": "^3.4.2", 81 | "ts-node": "^7.0.1", 82 | "typescript": "^4.5.5" 83 | }, 84 | "repository": { 85 | "type": "git", 86 | "url": "git+https://github.com/PayU/prometheus-api-metrics.git" 87 | }, 88 | "keywords": [ 89 | "monitoring", 90 | "nodejs", 91 | "node", 92 | "prometheus", 93 | "api", 94 | "express", 95 | "koa", 96 | "metrics" 97 | ], 98 | "license": "Apache-2.0", 99 | "bugs": { 100 | "url": "https://github.com/PayU/prometheus-api-metrics/issues" 101 | }, 102 | "homepage": "https://github.com/PayU/prometheus-api-metrics#readme" 103 | } 104 | -------------------------------------------------------------------------------- /src/express-middleware.js: -------------------------------------------------------------------------------- 1 | const Prometheus = require('prom-client'); 2 | require('pkginfo')(module, ['name']); 3 | const debug = require('debug')(module.exports.name); 4 | const utils = require('./utils'); 5 | 6 | class ExpressMiddleware { 7 | constructor(setupOptions) { 8 | this.setupOptions = setupOptions; 9 | } 10 | 11 | _collectDefaultServerMetrics(timeout) { 12 | const NUMBER_OF_CONNECTIONS_METRICS_NAME = 'expressjs_number_of_open_connections'; 13 | this.setupOptions.numberOfConnectionsGauge = Prometheus.register.getSingleMetric(NUMBER_OF_CONNECTIONS_METRICS_NAME) || new Prometheus.Gauge({ 14 | name: NUMBER_OF_CONNECTIONS_METRICS_NAME, 15 | help: 'Number of open connections to the Express.js server' 16 | }); 17 | if (this.setupOptions.server) { 18 | setInterval(this._getConnections.bind(this), timeout).unref(); 19 | } 20 | } 21 | 22 | _getConnections() { 23 | if (this.setupOptions && this.setupOptions.server) { 24 | this.setupOptions.server.getConnections((error, count) => { 25 | if (error) { 26 | debug('Error while collection number of open connections', error); 27 | } else { 28 | this.setupOptions.numberOfConnectionsGauge.set(count); 29 | } 30 | }); 31 | } 32 | } 33 | 34 | _handleResponse(req, res) { 35 | const responseLength = parseInt(res.get('Content-Length')) || 0; 36 | 37 | const route = this._getRoute(req); 38 | 39 | if (route && utils.shouldLogMetrics(this.setupOptions.excludeRoutes, route)) { 40 | const labels = { 41 | method: req.method, 42 | route, 43 | code: res.statusCode, 44 | ...this.setupOptions.extractAdditionalLabelValuesFn(req, res) 45 | }; 46 | this.setupOptions.requestSizeHistogram.observe(labels, req.metrics.contentLength); 47 | req.metrics.timer(labels); 48 | this.setupOptions.responseSizeHistogram.observe(labels, responseLength); 49 | debug(`metrics updated, request length: ${req.metrics.contentLength}, response length: ${responseLength}`); 50 | } 51 | } 52 | 53 | _getRoute(req) { 54 | let route = req.baseUrl; 55 | if (req.route) { 56 | if (req.route.path !== '/') { 57 | route = route ? route + req.route.path : req.route.path; 58 | } 59 | 60 | if (!route || route === '' || typeof route !== 'string') { 61 | route = req.originalUrl.split('?')[0]; 62 | } else { 63 | const splittedRoute = route.split('/'); 64 | const splittedUrl = req.originalUrl.split('?')[0].split('/'); 65 | const routeIndex = splittedUrl.length - splittedRoute.length + 1; 66 | 67 | const baseUrl = splittedUrl.slice(0, routeIndex).join('/'); 68 | route = baseUrl + route; 69 | } 70 | 71 | if (this.setupOptions.includeQueryParams === true && Object.keys(req.query).length > 0) { 72 | route = `${route}?${Object.keys(req.query).sort().map((queryParam) => `${queryParam}=`).join('&')}`; 73 | } 74 | } 75 | 76 | // nest.js - build request url pattern if exists 77 | if (typeof req.params === 'object') { 78 | Object.keys(req.params).forEach((paramName) => { 79 | route = route.replace(req.params[paramName], ':' + paramName); 80 | }); 81 | } 82 | 83 | // this condition will evaluate to true only in 84 | // express framework and no route was found for the request. if we log this metrics 85 | // we'll risk in a memory leak since the route is not a pattern but a hardcoded string. 86 | if (!route || route === '') { 87 | // if (!req.route && res && res.statusCode === 404) { 88 | route = 'N/A'; 89 | } 90 | 91 | return route; 92 | } 93 | 94 | async middleware(req, res, next) { 95 | if (!this.setupOptions.server && req.socket) { 96 | this.setupOptions.server = req.socket.server; 97 | this._collectDefaultServerMetrics(this.setupOptions.defaultMetricsInterval); 98 | } 99 | 100 | const routeUrl = req.originalUrl || req.url; 101 | 102 | if (routeUrl === this.setupOptions.metricsRoute) { 103 | debug('Request to /metrics endpoint'); 104 | res.set('Content-Type', Prometheus.register.contentType); 105 | return res.end(await Prometheus.register.metrics()); 106 | } 107 | if (routeUrl === `${this.setupOptions.metricsRoute}.json`) { 108 | debug('Request to /metrics endpoint'); 109 | return res.json(await Prometheus.register.getMetricsAsJSON()); 110 | } 111 | 112 | req.metrics = { 113 | timer: this.setupOptions.responseTimeHistogram.startTimer(), 114 | contentLength: parseInt(req.get('content-length')) || 0 115 | }; 116 | 117 | debug(`Set start time and content length for request. url: ${routeUrl}, method: ${req.method}`); 118 | 119 | res.once('finish', () => { 120 | debug('on finish.'); 121 | this._handleResponse(req, res); 122 | }); 123 | 124 | return next(); 125 | }; 126 | } 127 | 128 | module.exports = ExpressMiddleware; 129 | -------------------------------------------------------------------------------- /src/index.d.ts: -------------------------------------------------------------------------------- 1 | import { Request, RequestHandler, Response } from 'express'; 2 | import { Context, Middleware } from 'koa'; 3 | 4 | export default function middleware(options?: ApiMetricsOpts) : RequestHandler; 5 | export function koaMiddleware(options?: ApiMetricsOpts) : Middleware; 6 | export function expressMiddleware(options?: ApiMetricsOpts) : RequestHandler; 7 | export class HttpMetricsCollector { 8 | constructor(options?: CollectorOpts) 9 | static init(options?: CollectorOpts): void 10 | static collect(res: Response | any): void 11 | } 12 | 13 | export interface ApiMetricsOpts { 14 | metricsPath?: string; 15 | defaultMetricsInterval?: number; 16 | durationBuckets?: number[]; 17 | requestSizeBuckets?: number[]; 18 | responseSizeBuckets?: number[]; 19 | useUniqueHistogramName?: boolean; 20 | metricsPrefix?: string; 21 | excludeRoutes?:string[]; 22 | includeQueryParams?: boolean; 23 | additionalLabels?: string[]; 24 | extractAdditionalLabelValuesFn?: ((req: Request, res: Response) => Record) | ((ctx: Context) => Record) 25 | } 26 | 27 | export interface CollectorOpts { 28 | durationBuckets?: number[]; 29 | countClientErrors?: boolean; 30 | useUniqueHistogramName?: boolean 31 | prefix?: string; 32 | } 33 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Path = require('path'); 4 | const metricsMiddleware = { exports: {} }; 5 | require('pkginfo')(metricsMiddleware, { dir: Path.dirname(module.parent.filename), include: ['name', 'version'] }); 6 | const appVersion = metricsMiddleware.exports.version; 7 | const projectName = metricsMiddleware.exports.name.replace(/-/g, '_'); 8 | 9 | module.exports = require('./metrics-middleware')(appVersion, projectName); 10 | module.exports.HttpMetricsCollector = require('./request-response-collector')(projectName); 11 | module.exports.koaMiddleware = require('./metrics-middleware')(appVersion, projectName, 'koa'); 12 | module.exports.expressMiddleware = require('./metrics-middleware')(appVersion, projectName, 'express'); 13 | -------------------------------------------------------------------------------- /src/koa-middleware.js: -------------------------------------------------------------------------------- 1 | const Prometheus = require('prom-client'); 2 | require('pkginfo')(module, ['name']); 3 | const debug = require('debug')(module.exports.name); 4 | const utils = require('./utils'); 5 | 6 | const WILDCARD_ROUTE_ENDING = '(.*)'; 7 | 8 | class KoaMiddleware { 9 | constructor(setupOptions) { 10 | this.setupOptions = setupOptions; 11 | } 12 | 13 | _collectDefaultServerMetrics(timeout) { 14 | const NUMBER_OF_CONNECTIONS_METRICS_NAME = 'koajs_number_of_open_connections'; 15 | this.setupOptions.numberOfConnectionsGauge = Prometheus.register.getSingleMetric(NUMBER_OF_CONNECTIONS_METRICS_NAME) || new Prometheus.Gauge({ 16 | name: NUMBER_OF_CONNECTIONS_METRICS_NAME, 17 | help: 'Number of open connections to the Koa.js server' 18 | }); 19 | if (this.setupOptions.server) { 20 | setInterval(this._getConnections.bind(this), timeout).unref(); 21 | } 22 | } 23 | 24 | _getConnections() { 25 | if (this.setupOptions.server) { 26 | this.setupOptions.server.getConnections((error, count) => { 27 | if (error) { 28 | debug('Error while collection number of open connections', error); 29 | } else { 30 | this.setupOptions.numberOfConnectionsGauge.set(count); 31 | } 32 | }); 33 | } 34 | } 35 | 36 | _handleResponse(ctx) { 37 | const responseLength = parseInt(ctx.response.get('Content-Length')) || 0; 38 | 39 | const route = this._getRoute(ctx) || 'N/A'; 40 | 41 | if (route && utils.shouldLogMetrics(this.setupOptions.excludeRoutes, route)) { 42 | const labels = { 43 | method: ctx.req.method, 44 | route, 45 | code: ctx.res.statusCode, 46 | ...this.setupOptions.extractAdditionalLabelValuesFn(ctx) 47 | }; 48 | this.setupOptions.requestSizeHistogram.observe(labels, ctx.req.metrics.contentLength); 49 | ctx.req.metrics.timer(labels); 50 | this.setupOptions.responseSizeHistogram.observe(labels, responseLength); 51 | debug(`metrics updated, request length: ${ctx.req.metrics.contentLength}, response length: ${responseLength}`); 52 | } 53 | } 54 | 55 | _getRoute(ctx) { 56 | let route; 57 | if (ctx._matchedRoute && !ctx._matchedRoute.endsWith(WILDCARD_ROUTE_ENDING)) { 58 | route = ctx._matchedRoute; 59 | route = route.endsWith('/') ? route.substring(0, route.length - 1) : route; 60 | } else if (ctx._matchedRoute) { 61 | route = this._handleSubRoutes(ctx._matchedRoute, ctx.originalUrl, ctx.request.method, ctx.router); 62 | } 63 | 64 | if (this.setupOptions.includeQueryParams === true && Object.keys(ctx.query).length > 0) { 65 | route = `${route || '/'}?${Object.keys(ctx.query).sort().map((queryParam) => `${queryParam}=`).join('&')}`; 66 | } 67 | 68 | return route; 69 | } 70 | 71 | _handleSubRoutes(matchedRoute, originalUrl, method, router) { 72 | let route; 73 | const routeStart = matchedRoute.substring(0, matchedRoute.length - WILDCARD_ROUTE_ENDING.length); 74 | let url = this._removeQueryFromUrl(originalUrl).substring(routeStart.length); 75 | let matchedRoutes = router.match(url, method); 76 | if (matchedRoutes.path.length > 0) { 77 | route = this._findFirstProperRoute(matchedRoutes.path); 78 | return routeStart + route; 79 | } else { 80 | url = this._removeQueryFromUrl(originalUrl); 81 | matchedRoutes = router.match(url, method); 82 | if (matchedRoutes.path.length > 0) { 83 | route = this._findFirstProperRoute(matchedRoutes.path); 84 | return route; 85 | } 86 | } 87 | } 88 | 89 | _findFirstProperRoute(routes) { 90 | const properRoute = routes.find(route => { 91 | if (!route.path.endsWith('(.*)')) { 92 | return route; 93 | } 94 | }); 95 | 96 | // If proper route is not found, send an undefined route 97 | // The caller is responsible for setting a default "N/A" route in this case 98 | if (!properRoute) { 99 | return undefined; 100 | } 101 | 102 | let route = properRoute.path; 103 | route = route.endsWith('/') ? route.substring(0, route.length - 1) : route; 104 | return route; 105 | } 106 | 107 | _removeQueryFromUrl(url) { 108 | return url.split('?')[0]; 109 | } 110 | 111 | async middleware(ctx, next) { 112 | if (!this.setupOptions.server && ctx.req.socket) { 113 | this.setupOptions.server = ctx.req.socket.server; 114 | this._collectDefaultServerMetrics(this.setupOptions.defaultMetricsInterval); 115 | } 116 | if (ctx.req.url === this.setupOptions.metricsRoute) { 117 | debug('Request to /metrics endpoint'); 118 | ctx.set('Content-Type', Prometheus.register.contentType); 119 | ctx.body = await Prometheus.register.metrics(); 120 | return next(); 121 | } 122 | if (ctx.req.url === `${this.setupOptions.metricsRoute}.json`) { 123 | debug('Request to /metrics endpoint'); 124 | ctx.body = await Prometheus.register.getMetricsAsJSON(); 125 | return next(); 126 | } 127 | 128 | ctx.req.metrics = { 129 | timer: this.setupOptions.responseTimeHistogram.startTimer(), 130 | contentLength: parseInt(ctx.request.get('content-length')) || 0 131 | }; 132 | 133 | debug(`Set start time and content length for request. url: ${ctx.req.url}, method: ${ctx.req.method}`); 134 | 135 | ctx.res.once('finish', () => { 136 | debug('on finish.'); 137 | this._handleResponse(ctx); 138 | }); 139 | 140 | return next(); 141 | }; 142 | } 143 | 144 | module.exports = KoaMiddleware; 145 | -------------------------------------------------------------------------------- /src/metrics-middleware.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Prometheus = require('prom-client'); 4 | require('pkginfo')(module, ['name']); 5 | const debug = require('debug')(module.exports.name); 6 | const utils = require('./utils'); 7 | const ExpressMiddleware = require('./express-middleware'); 8 | const KoaMiddleware = require('./koa-middleware'); 9 | const setupOptions = {}; 10 | 11 | module.exports = (appVersion, projectName, framework = 'express') => { 12 | return (options = {}) => { 13 | const { 14 | metricsPath, 15 | defaultMetricsInterval = 10000, 16 | durationBuckets, 17 | requestSizeBuckets, 18 | responseSizeBuckets, 19 | useUniqueHistogramName, 20 | metricsPrefix, 21 | excludeRoutes, 22 | includeQueryParams, 23 | additionalLabels = [], 24 | extractAdditionalLabelValuesFn 25 | } = options; 26 | debug(`Init metrics middleware with options: ${JSON.stringify(options)}`); 27 | 28 | setupOptions.metricsRoute = utils.validateInput({ 29 | input: metricsPath, 30 | isValidInputFn: utils.isString, 31 | defaultValue: '/metrics', 32 | errorMessage: 'metricsPath should be an string' 33 | }); 34 | 35 | setupOptions.excludeRoutes = utils.validateInput({ 36 | input: excludeRoutes, 37 | isValidInputFn: utils.isArray, 38 | defaultValue: [], 39 | errorMessage: 'excludeRoutes should be an array' 40 | }); 41 | 42 | setupOptions.includeQueryParams = includeQueryParams; 43 | setupOptions.defaultMetricsInterval = defaultMetricsInterval; 44 | 45 | setupOptions.additionalLabels = utils.validateInput({ 46 | input: additionalLabels, 47 | isValidInputFn: utils.isArray, 48 | defaultValue: [], 49 | errorMessage: 'additionalLabels should be an array' 50 | }); 51 | 52 | setupOptions.extractAdditionalLabelValuesFn = utils.validateInput({ 53 | input: extractAdditionalLabelValuesFn, 54 | isValidInputFn: utils.isFunction, 55 | defaultValue: () => ({}), 56 | errorMessage: 'extractAdditionalLabelValuesFn should be a function' 57 | }); 58 | 59 | const metricNames = utils.getMetricNames( 60 | { 61 | http_request_duration_seconds: 'http_request_duration_seconds', 62 | app_version: 'app_version', 63 | http_request_size_bytes: 'http_request_size_bytes', 64 | http_response_size_bytes: 'http_response_size_bytes', 65 | defaultMetricsPrefix: '' 66 | }, 67 | useUniqueHistogramName, 68 | metricsPrefix, 69 | projectName 70 | ); 71 | 72 | Prometheus.collectDefaultMetrics({ timeout: defaultMetricsInterval, prefix: `${metricNames.defaultMetricsPrefix}` }); 73 | 74 | PrometheusRegisterAppVersion(appVersion, metricNames.app_version); 75 | 76 | const metricLabels = [ 77 | 'method', 78 | 'route', 79 | 'code', 80 | ...additionalLabels 81 | ].filter(Boolean); 82 | 83 | // Buckets for response time from 1ms to 500ms 84 | const defaultDurationSecondsBuckets = [0.001, 0.005, 0.015, 0.05, 0.1, 0.2, 0.3, 0.4, 0.5]; 85 | // Buckets for request size from 5 bytes to 10000 bytes 86 | const defaultSizeBytesBuckets = [5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000, 10000]; 87 | 88 | setupOptions.responseTimeHistogram = Prometheus.register.getSingleMetric(metricNames.http_request_duration_seconds) || new Prometheus.Histogram({ 89 | name: metricNames.http_request_duration_seconds, 90 | help: 'Duration of HTTP requests in seconds', 91 | labelNames: metricLabels, 92 | buckets: durationBuckets || defaultDurationSecondsBuckets 93 | }); 94 | 95 | setupOptions.requestSizeHistogram = Prometheus.register.getSingleMetric(metricNames.http_request_size_bytes) || new Prometheus.Histogram({ 96 | name: metricNames.http_request_size_bytes, 97 | help: 'Size of HTTP requests in bytes', 98 | labelNames: metricLabels, 99 | buckets: requestSizeBuckets || defaultSizeBytesBuckets 100 | }); 101 | 102 | setupOptions.responseSizeHistogram = Prometheus.register.getSingleMetric(metricNames.http_response_size_bytes) || new Prometheus.Histogram({ 103 | name: metricNames.http_response_size_bytes, 104 | help: 'Size of HTTP response in bytes', 105 | labelNames: metricLabels, 106 | buckets: responseSizeBuckets || defaultSizeBytesBuckets 107 | }); 108 | 109 | return frameworkMiddleware(framework); 110 | }; 111 | }; 112 | 113 | function PrometheusRegisterAppVersion(appVersion, metricName) { 114 | const version = new Prometheus.Gauge({ 115 | name: metricName, 116 | help: 'The service version by package.json', 117 | labelNames: ['version', 'major', 'minor', 'patch'] 118 | }); 119 | 120 | const [major, minor, patch] = appVersion.split('.').map(Number); 121 | version.labels(appVersion, major, minor, patch).set(1); 122 | } 123 | 124 | function frameworkMiddleware (framework) { 125 | switch (framework) { 126 | case 'koa': { 127 | const middleware = new KoaMiddleware(setupOptions); 128 | return middleware.middleware.bind(middleware); 129 | } 130 | default: { 131 | const middleware = new ExpressMiddleware(setupOptions); 132 | return middleware.middleware.bind(middleware); 133 | } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/request-response-collector.js: -------------------------------------------------------------------------------- 1 | const Prometheus = require('prom-client'); 2 | const utils = require('./utils'); 3 | const get = require('lodash.get'); 4 | 5 | let southboundResponseTimeHistogram, southboundClientErrors = null; 6 | let projectName; 7 | 8 | module.exports = (name) => { 9 | projectName = name; 10 | const httpMetricsCollector = HttpMetricsCollector; 11 | httpMetricsCollector.init = init; 12 | httpMetricsCollector.collect = collect; 13 | 14 | return httpMetricsCollector; 15 | }; 16 | 17 | const OBSERVER_TYPES = ['total', 'socket', 'lookup', 'connect']; 18 | 19 | class HttpMetricsCollector { 20 | constructor(options){ 21 | const setup = _init(options); 22 | this.southboundResponseTimeHistogram = setup.southboundResponseTimeHistogram; 23 | this.southboundClientErrors = setup.southboundClientErrors; 24 | }; 25 | 26 | collect(res) { 27 | _collectHttpTiming(res, this.southboundResponseTimeHistogram, this.southboundClientErrors); 28 | } 29 | } 30 | 31 | function _collectHttpTiming(res, southboundResponseTimeHistogram, southboundClientErrors) { 32 | if (res instanceof Error && !res.response && southboundClientErrors) { 33 | const error = res.error || res; 34 | southboundClientErrors.inc({ target: error.hostname, error: error.code }); 35 | } else { 36 | const response = res.response || res; 37 | if (response.timings) { 38 | const responseData = extractResponseData(response); 39 | addObservers(southboundResponseTimeHistogram, responseData); 40 | } 41 | } 42 | } 43 | 44 | function addObservers(southboundResponseTimeHistogram, responseData) { 45 | const { target, method, route, status_code, timings } = responseData; 46 | 47 | OBSERVER_TYPES.forEach(type => { 48 | if (typeof responseData.timings[type] !== 'undefined') { 49 | southboundResponseTimeHistogram.observe({ target, method, route, status_code, type }, timings[type]); 50 | } 51 | }); 52 | } 53 | 54 | function extractResponseData(response) { 55 | let status_code, route, method, target, timings; 56 | 57 | // check if response client is axios 58 | if (isAxiosResponse(response)) { 59 | status_code = response.status; 60 | method = response.config.method.toUpperCase(); 61 | route = get(response, 'config.metrics.route', response.config.url); 62 | target = get(response, 'config.metrics.target', response.config.baseURL); 63 | timings = { 64 | total: response.timings.elapsedTime / 1000 65 | }; 66 | } else { // response is request-promise 67 | status_code = response.statusCode; 68 | method = response.request.method; 69 | route = get(response, 'request.metrics.route', response.request.path); 70 | target = get(response, 'request.metrics.target', response.request.originalHost); 71 | timings = { 72 | total: response.timingPhases.total / 1000, 73 | socket: response.timingPhases.wait / 1000, 74 | lookup: response.timingPhases.dns / 1000, 75 | connect: response.timingPhases.tcp / 1000 76 | }; 77 | } 78 | 79 | return { 80 | target, 81 | method, 82 | route, 83 | status_code, 84 | timings 85 | }; 86 | } 87 | 88 | function isAxiosResponse(response) { 89 | return response.config && response.hasOwnProperty('data'); 90 | } 91 | 92 | function _init(options = {}) { 93 | let metricNames = { 94 | southbound_request_duration_seconds: 'southbound_request_duration_seconds', 95 | southbound_client_errors_count: 'southbound_client_errors_count' 96 | }; 97 | 98 | const { durationBuckets, countClientErrors, useUniqueHistogramName, prefix } = options; 99 | metricNames = utils.getMetricNames(metricNames, useUniqueHistogramName, prefix, projectName); 100 | 101 | southboundResponseTimeHistogram = Prometheus.register.getSingleMetric(metricNames.southbound_request_duration_seconds) || 102 | new Prometheus.Histogram({ 103 | name: metricNames.southbound_request_duration_seconds, 104 | help: 'Duration of Southbound queries in seconds', 105 | labelNames: ['method', 'route', 'status_code', 'target', 'type'], 106 | buckets: durationBuckets || [0.001, 0.005, 0.015, 0.03, 0.05, 0.1, 0.15, 0.3, 0.5] 107 | }); 108 | 109 | if (countClientErrors !== false) { 110 | southboundClientErrors = Prometheus.register.getSingleMetric(metricNames.southbound_client_errors_count) || new Prometheus.Counter({ 111 | name: metricNames.southbound_client_errors_count, 112 | help: 'Southbound http client error counter', 113 | labelNames: ['target', 'error'] 114 | }); 115 | } 116 | 117 | return { 118 | southboundClientErrors: southboundClientErrors, 119 | southboundResponseTimeHistogram: southboundResponseTimeHistogram 120 | }; 121 | }; 122 | 123 | function init(options) { 124 | const setup = _init(options); 125 | southboundResponseTimeHistogram = setup.southboundResponseTimeHistogram; 126 | southboundClientErrors = setup.southboundClientErrors; 127 | }; 128 | function collect(res) { 129 | _collectHttpTiming(res, southboundResponseTimeHistogram, southboundClientErrors); 130 | }; 131 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const getMetricNames = (metricNames, useUniqueHistogramName, metricsPrefix, projectName) => { 4 | const prefix = useUniqueHistogramName === true ? projectName : metricsPrefix; 5 | 6 | if (prefix) { 7 | Object.keys(metricNames).forEach(key => { 8 | metricNames[key] = `${prefix}_${metricNames[key]}`; 9 | }); 10 | } 11 | 12 | return metricNames; 13 | }; 14 | 15 | const isArray = (input) => Array.isArray(input); 16 | 17 | const isFunction = (input) => typeof input === 'function'; 18 | 19 | const isString = (input) => typeof input === 'string'; 20 | 21 | const shouldLogMetrics = (excludeRoutes, route) => excludeRoutes.every((path) => !route.includes(path)); 22 | 23 | const validateInput = ({ input, isValidInputFn, defaultValue, errorMessage }) => { 24 | if (typeof input !== 'undefined') { 25 | if (isValidInputFn(input)) { 26 | return input; 27 | } else { 28 | throw new Error(errorMessage); 29 | } 30 | } 31 | 32 | return defaultValue; 33 | }; 34 | 35 | module.exports.getMetricNames = getMetricNames; 36 | module.exports.isArray = isArray; 37 | module.exports.isFunction = isFunction; 38 | module.exports.isString = isString; 39 | module.exports.shouldLogMetrics = shouldLogMetrics; 40 | module.exports.validateInput = validateInput; 41 | -------------------------------------------------------------------------------- /test/integration-tests/express/middleware-exclude-routes-test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const Prometheus = require('prom-client'); 3 | const expect = require('chai').expect; 4 | const supertest = require('supertest'); 5 | let app, config; 6 | 7 | describe('when using express framework (exclude route)', function() { 8 | this.timeout(4000); 9 | before(() => { 10 | config = require('./server/config'); 11 | config.useUniqueHistogramName = true; 12 | app = require('./server/express-server-exclude-routes'); 13 | }); 14 | after(() => { 15 | Prometheus.register.clear(); 16 | delete require.cache[require.resolve('./server/express-server-exclude-routes.js')]; 17 | delete require.cache[require.resolve('../../../src/index.js')]; 18 | delete require.cache[require.resolve('../../../src/metrics-middleware.js')]; 19 | }); 20 | describe('when start up with exclude route', () => { 21 | describe('when calling a GET endpoint', () => { 22 | before(() => { 23 | return supertest(app) 24 | .get('/hello') 25 | .expect(200) 26 | .then((res) => {}); 27 | }); 28 | it('should add it to the histogram', () => { 29 | return supertest(app) 30 | .get('/metrics') 31 | .expect(200) 32 | .then((res) => { 33 | expect(res.text).to.contain('method="GET",route="/hello",code="200"'); 34 | }); 35 | }); 36 | }); 37 | describe('when calling a GET endpoint of excluded path', () => { 38 | before(() => { 39 | return supertest(app) 40 | .get('/health') 41 | .expect(200) 42 | .then((res) => {}); 43 | }); 44 | it('should add it to the histogram', () => { 45 | return supertest(app) 46 | .get('/metrics') 47 | .expect(200) 48 | .then((res) => { 49 | expect(res.text).to.not.contain('method="GET",route="/health",code="200"'); 50 | }); 51 | }); 52 | }); 53 | describe('when calling a GET endpoint of excluded path with variables', () => { 54 | before(() => { 55 | return supertest(app) 56 | .get('/health/1234') 57 | .expect(200) 58 | .then((res) => {}); 59 | }); 60 | it('should add it to the histogram', () => { 61 | return supertest(app) 62 | .get('/metrics') 63 | .expect(200) 64 | .then((res) => { 65 | expect(res.text).to.not.contain('method="GET",route="/health/:id",code="200"'); 66 | }); 67 | }); 68 | }); 69 | }); 70 | describe('when start up with include query params', () => { 71 | describe('when calling a GET endpoint with one query param', () => { 72 | before(() => { 73 | return supertest(app) 74 | .get('/hello?test=test') 75 | .expect(200) 76 | .then((res) => {}); 77 | }); 78 | it('should add it to the histogram', () => { 79 | return supertest(app) 80 | .get('/metrics') 81 | .expect(200) 82 | .then((res) => { 83 | expect(res.text).to.contain('method="GET",route="/hello?test=",code="200"'); 84 | }); 85 | }); 86 | }); 87 | describe('when calling a GET endpoint with two query params', () => { 88 | before(() => { 89 | return supertest(app) 90 | .get('/hello?test1=test&test2=test2') 91 | .expect(200) 92 | .then((res) => {}); 93 | }); 94 | it('should add it to the histogram and sort the query params', () => { 95 | return supertest(app) 96 | .get('/metrics') 97 | .expect(200) 98 | .then((res) => { 99 | expect(res.text).to.contain('http_request_duration_seconds_count{method="GET",route="/hello?test1=&test2=",code="200"} 1'); 100 | }); 101 | }); 102 | }); 103 | describe('when calling a GET endpoint with two query params in different order', () => { 104 | before(() => { 105 | return supertest(app) 106 | .get('/hello?test2=test&test1=test2') 107 | .expect(200) 108 | .then((res) => {}); 109 | }); 110 | it('should add it to the histogram and sort the query params', () => { 111 | return supertest(app) 112 | .get('/metrics') 113 | .expect(200) 114 | .then((res) => { 115 | expect(res.text).to.contain('http_request_duration_seconds_count{method="GET",route="/hello?test1=&test2=",code="200"} 2'); 116 | }); 117 | }); 118 | }); 119 | describe('when calling a GET endpoint with query param', () => { 120 | before(() => { 121 | return supertest(app) 122 | .get('/health/1234?test=test') 123 | .expect(200) 124 | .then((res) => {}); 125 | }); 126 | it('should add it to the histogram', () => { 127 | return supertest(app) 128 | .get('/metrics') 129 | .expect(200) 130 | .then((res) => { 131 | expect(res.text).to.not.contain('method="GET",route="/health/:id?test=",code="200"'); 132 | }); 133 | }); 134 | }); 135 | describe('when calling a GET root endpoint with query param ', () => { 136 | before(() => { 137 | return supertest(app) 138 | .get('/?test=test') 139 | .expect(200) 140 | .then((res) => {}); 141 | }); 142 | it('should add it to the histogram', () => { 143 | return supertest(app) 144 | .get('/metrics') 145 | .expect(200) 146 | .then((res) => { 147 | expect(res.text).to.contain('http_request_size_bytes_count{method="GET",route="/?test=",code="200"} 1'); 148 | }); 149 | }); 150 | }); 151 | }); 152 | }); 153 | -------------------------------------------------------------------------------- /test/integration-tests/express/middleware-test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const Prometheus = require('prom-client'); 3 | const expect = require('chai').expect; 4 | const supertest = require('supertest'); 5 | let app, config; 6 | 7 | describe('when using express framework', () => { 8 | before(() => { 9 | app = require('./server/express-server'); 10 | config = require('./server/config'); 11 | }); 12 | after(() => { 13 | Prometheus.register.clear(); 14 | delete require.cache[require.resolve('./server/express-server')]; 15 | delete require.cache[require.resolve('../../../src/index.js')]; 16 | delete require.cache[require.resolve('../../../src/metrics-middleware.js')]; 17 | }); 18 | describe('when start up', () => { 19 | it('should populate default metrics', () => { 20 | return supertest(app) 21 | .get('/metrics') 22 | .expect(200) 23 | .then((res) => { 24 | expect(res.text).to.contain('process_cpu_user_seconds_total'); 25 | expect(res.text).to.contain('process_cpu_system_seconds_total'); 26 | expect(res.text).to.contain('process_cpu_seconds_total'); 27 | expect(res.text).to.contain('process_start_time_seconds'); 28 | expect(res.text).to.contain('process_resident_memory_bytes'); 29 | expect(res.text).to.contain('nodejs_eventloop_lag_seconds'); 30 | 31 | expect(res.text).to.contain('nodejs_active_handles_total'); 32 | expect(res.text).to.contain('nodejs_active_requests_total'); 33 | 34 | expect(res.text).to.contain('nodejs_heap_size_total_bytes'); 35 | expect(res.text).to.contain('nodejs_heap_size_used_bytes'); 36 | expect(res.text).to.contain('nodejs_external_memory_bytes'); 37 | 38 | expect(res.text).to.contain('nodejs_heap_space_size_total_bytes{space="new"}'); 39 | expect(res.text).to.contain('nodejs_heap_space_size_total_bytes{space="old"}'); 40 | expect(res.text).to.contain('nodejs_heap_space_size_total_bytes{space="code"}'); 41 | expect(res.text).to.contain('nodejs_heap_space_size_total_bytes{space="large_object"}'); 42 | 43 | expect(res.text).to.contain('nodejs_heap_space_size_used_bytes{space="new"}'); 44 | expect(res.text).to.contain('nodejs_heap_space_size_used_bytes{space="old"}'); 45 | expect(res.text).to.contain('nodejs_heap_space_size_used_bytes{space="code"}'); 46 | expect(res.text).to.contain('nodejs_heap_space_size_used_bytes{space="large_object"}'); 47 | 48 | expect(res.text).to.contain('nodejs_heap_space_size_available_bytes{space="new"}'); 49 | expect(res.text).to.contain('nodejs_heap_space_size_available_bytes{space="old"}'); 50 | expect(res.text).to.contain('nodejs_heap_space_size_available_bytes{space="code"}'); 51 | expect(res.text).to.contain('nodejs_heap_space_size_available_bytes{space="large_object"}'); 52 | 53 | expect(res.text).to.contain('nodejs_version_info'); 54 | expect(res.text).to.contain('app_version{version="1.0.0",major="1",minor="0",patch="0"}'); 55 | }); 56 | }); 57 | describe('when calling a GET endpoint', () => { 58 | before(() => { 59 | return supertest(app) 60 | .get('/hello') 61 | .expect(200) 62 | .then((res) => {}); 63 | }); 64 | it('should add it to the histogram', () => { 65 | return supertest(app) 66 | .get('/metrics') 67 | .expect(200) 68 | .then((res) => { 69 | expect(res.text).to.contain('method="GET",route="/hello",code="200"'); 70 | }); 71 | }); 72 | }); 73 | describe('when calling a GET endpoint', () => { 74 | before(() => { 75 | return supertest(app) 76 | .get('/hello') 77 | .expect(200) 78 | .then((res) => {}); 79 | }); 80 | it('should add number of open connections', () => { 81 | return supertest(app) 82 | .get('/metrics') 83 | .expect(200) 84 | .then((res) => { 85 | expect(res.text).to.contain('expressjs_number_of_open_connections'); 86 | }); 87 | }); 88 | }); 89 | describe('when calling a GET endpoint with path params', () => { 90 | before(() => { 91 | return supertest(app) 92 | .get('/hello/200') 93 | .expect(200) 94 | .then((res) => {}); 95 | }); 96 | it('should add it to the histogram', () => { 97 | return supertest(app) 98 | .get('/metrics') 99 | .expect(200) 100 | .then((res) => { 101 | expect(res.text).to.contain('http_request_duration_seconds_bucket{le="+Inf",method="GET",route="/hello/:time",code="200"} 1'); 102 | expect(res.text).to.contain('http_response_size_bytes_bucket{le="+Inf",method="GET",route="/hello/:time",code="200"} 1'); 103 | expect(res.text).to.contain('http_request_size_bytes_bucket{le="+Inf",method="GET",route="/hello/:time",code="200"} 1'); 104 | }); 105 | }); 106 | }); 107 | describe('when calling a POST endpoint', () => { 108 | before(() => { 109 | return supertest(app) 110 | .post('/test') 111 | .send({ name: 'john' }) 112 | .set('Accept', 'application/json') 113 | .expect(201) 114 | .then((res) => {}); 115 | }); 116 | it('should add it to the histogram', () => { 117 | return supertest(app) 118 | .get('/metrics') 119 | .expect(200) 120 | .then((res) => { 121 | expect(res.text).to.contain('method="POST",route="/test",code="201"'); 122 | }); 123 | }); 124 | }); 125 | describe('when calling endpoint and getting an error', () => { 126 | before(() => { 127 | return supertest(app) 128 | .get('/bad') 129 | .expect(500) 130 | .then((res) => {}); 131 | }); 132 | it('should add it to the histogram', () => { 133 | return supertest(app) 134 | .get('/metrics') 135 | .expect(200) 136 | .then((res) => { 137 | expect(res.text).to.contain('method="GET",route="/bad",code="500"'); 138 | }); 139 | }); 140 | }); 141 | describe('when calling a GET endpoint with query parmas', () => { 142 | before(() => { 143 | return supertest(app) 144 | .get('/hello?test=test') 145 | .expect(200) 146 | .then((res) => {}); 147 | }); 148 | it('should add it to the histogram', () => { 149 | return supertest(app) 150 | .get('/metrics') 151 | .expect(200) 152 | .then((res) => { 153 | expect(res.text).to.contain('http_request_duration_seconds_bucket{le="+Inf",method="GET",route="/hello",code="200"} 3'); 154 | }); 155 | }); 156 | }); 157 | describe('sub app', function () { 158 | describe('when calling a GET endpoint with path params', () => { 159 | before(() => { 160 | return supertest(app) 161 | .get('/v2/hello/200') 162 | .expect(200) 163 | .then((res) => {}); 164 | }); 165 | it('should add it to the histogram', () => { 166 | return supertest(app) 167 | .get('/metrics') 168 | .expect(200) 169 | .then((res) => { 170 | expect(res.text).to.contain('http_request_duration_seconds_bucket{le="+Inf",method="GET",route="/v2/hello/:time",code="200"} 1'); 171 | expect(res.text).to.contain('http_response_size_bytes_bucket{le="+Inf",method="GET",route="/v2/hello/:time",code="200"} 1'); 172 | expect(res.text).to.contain('http_request_size_bytes_bucket{le="+Inf",method="GET",route="/v2/hello/:time",code="200"} 1'); 173 | }); 174 | }); 175 | }); 176 | describe('when calling a POST endpoint', () => { 177 | before(() => { 178 | return supertest(app) 179 | .post('/v2/test') 180 | .send({ name: 'john' }) 181 | .set('Accept', 'application/json') 182 | .expect(201) 183 | .then((res) => {}); 184 | }); 185 | it('should add it to the histogram', () => { 186 | return supertest(app) 187 | .get('/metrics') 188 | .expect(200) 189 | .then((res) => { 190 | expect(res.text).to.contain('method="POST",route="/v2/test",code="201"'); 191 | }); 192 | }); 193 | }); 194 | describe('when calling endpoint and getting an error only variables', () => { 195 | before(() => { 196 | return supertest(app) 197 | .patch('/v2/500') 198 | .expect(500) 199 | .then((res) => {}); 200 | }); 201 | it('should add it to the histogram', () => { 202 | return supertest(app) 203 | .get('/metrics') 204 | .expect(200) 205 | .then((res) => { 206 | expect(res.text).to.contain('method="PATCH",route="/v2/:time",code="500"'); 207 | }); 208 | }); 209 | }); 210 | describe('when calling endpoint and getting an error with 1 variable', () => { 211 | before(() => { 212 | return supertest(app) 213 | .get('/v2/bad/500') 214 | .expect(500) 215 | .then((res) => {}); 216 | }); 217 | it('should add it to the histogram', () => { 218 | return supertest(app) 219 | .get('/metrics') 220 | .expect(200) 221 | .then((res) => { 222 | expect(res.text).to.contain('method="GET",route="/v2/bad/:time",code="500"'); 223 | }); 224 | }); 225 | }); 226 | describe('when calling endpoint and getting an error with two variables', () => { 227 | before(() => { 228 | return supertest(app) 229 | .get('/v2/bad/500/400') 230 | .expect(500) 231 | .then((res) => {}); 232 | }); 233 | it('should add it to the histogram', () => { 234 | return supertest(app) 235 | .get('/metrics') 236 | .expect(200) 237 | .then((res) => { 238 | expect(res.text).to.contain('method="GET",route="/v2/bad/:var1/:var2",code="500"'); 239 | }); 240 | }); 241 | }); 242 | describe('when calling endpoint and getting an error with no variables', () => { 243 | before(() => { 244 | return supertest(app) 245 | .get('/v2/bad') 246 | .expect(500) 247 | .then((res) => {}); 248 | }); 249 | it('should add it to the histogram', () => { 250 | return supertest(app) 251 | .get('/metrics') 252 | .expect(200) 253 | .then((res) => { 254 | expect(res.text).to.contain('method="GET",route="/v2/bad",code="500"'); 255 | }); 256 | }); 257 | }); 258 | describe('when calling endpoint and getting an error (root)', () => { 259 | before(() => { 260 | return supertest(app) 261 | .get('/v2') 262 | .expect(500) 263 | .then((res) => {}); 264 | }); 265 | it('should add it to the histogram', () => { 266 | return supertest(app) 267 | .get('/metrics') 268 | .expect(200) 269 | .then((res) => { 270 | expect(res.text).to.contain('method="GET",route="/v2",code="500"'); 271 | }); 272 | }); 273 | }); 274 | describe('when calling endpoint and getting an error (error handler in the sub app)', () => { 275 | before(() => { 276 | return supertest(app) 277 | .get('/v2/error/500') 278 | .expect(500) 279 | .then((res) => {}); 280 | }); 281 | it('should add it to the histogram', () => { 282 | return supertest(app) 283 | .get('/metrics') 284 | .expect(200) 285 | .then((res) => { 286 | expect(res.text).to.contain('method="GET",route="/v2/error/:var1",code="500"'); 287 | }); 288 | }); 289 | }); 290 | describe('when calling endpoint and getting an error from a middleware before sub route', () => { 291 | before(() => { 292 | return supertest(app) 293 | .get('/v2/hello') 294 | .set('error', 'error') 295 | .expect(500) 296 | .then((res) => {}); 297 | }); 298 | it('should add it to the histogram', () => { 299 | return supertest(app) 300 | .get('/metrics') 301 | .expect(200) 302 | .then((res) => { 303 | expect(res.text).to.contain('method="GET",route="/v2",code="500"'); 304 | }); 305 | }); 306 | }); 307 | describe('when calling a GET endpoint with query params', () => { 308 | before(() => { 309 | return supertest(app) 310 | .get('/v2?test=test') 311 | .expect(500) 312 | .then((res) => {}); 313 | }); 314 | it('should add it to the histogram', () => { 315 | return supertest(app) 316 | .get('/metrics') 317 | .expect(200) 318 | .then((res) => { 319 | expect(res.text).to.contain('http_request_duration_seconds_bucket{le="+Inf",method="GET",route="/v2",code="500"} 2'); 320 | }); 321 | }); 322 | }); 323 | describe('when calling a GET endpoint with query params with special character /', () => { 324 | before(() => { 325 | return supertest(app) 326 | .get('/checkout?test=test/test1') 327 | .expect(200) 328 | .then((res) => { }); 329 | }); 330 | it('should add it to the histogram', () => { 331 | return supertest(app) 332 | .get('/metrics') 333 | .expect(200) 334 | .then((res) => { 335 | expect(res.text).to.contain('http_request_duration_seconds_bucket{le="+Inf",method="GET",route="/checkout",code="200"} 1'); 336 | }); 337 | }); 338 | }); 339 | }); 340 | describe('sub-sub app with error handler in the sub app', function () { 341 | describe('when calling a GET endpoint with path params and sub router', () => { 342 | before(() => { 343 | return supertest(app) 344 | .get('/v2/v3/hello/200') 345 | .expect(200) 346 | .then((res) => {}); 347 | }); 348 | it('should add it to the histogram', () => { 349 | return supertest(app) 350 | .get('/metrics') 351 | .expect(200) 352 | .then((res) => { 353 | expect(res.text).to.contain('http_request_duration_seconds_bucket{le="+Inf",method="GET",route="/v2/v3/hello/:time",code="200"} 1'); 354 | expect(res.text).to.contain('http_response_size_bytes_bucket{le="+Inf",method="GET",route="/v2/v3/hello/:time",code="200"} 1'); 355 | expect(res.text).to.contain('http_request_size_bytes_bucket{le="+Inf",method="GET",route="/v2/v3/hello/:time",code="200"} 1'); 356 | }); 357 | }); 358 | }); 359 | describe('when calling a POST endpoint with sub router', () => { 360 | before(() => { 361 | return supertest(app) 362 | .post('/v2/v3/test') 363 | .send({ name: 'john' }) 364 | .set('Accept', 'application/json') 365 | .expect(201) 366 | .then((res) => {}); 367 | }); 368 | it('should add it to the histogram', () => { 369 | return supertest(app) 370 | .get('/metrics') 371 | .expect(200) 372 | .then((res) => { 373 | expect(res.text).to.contain('method="POST",route="/v2/v3/test",code="201"'); 374 | }); 375 | }); 376 | }); 377 | describe('when calling endpoint and getting an error with sub router only variables', () => { 378 | before(() => { 379 | return supertest(app) 380 | .patch('/v2/v3/500') 381 | .expect(500) 382 | .then((res) => {}); 383 | }); 384 | it('should add it to the histogram', () => { 385 | return supertest(app) 386 | .get('/metrics') 387 | .expect(200) 388 | .then((res) => { 389 | expect(res.text).to.contain('method="PATCH",route="/v2/v3/:time",code="500"'); 390 | }); 391 | }); 392 | }); 393 | describe('when calling endpoint and getting an error with sub router with 1 variable', () => { 394 | before(() => { 395 | return supertest(app) 396 | .get('/v2/v3/bad/500') 397 | .expect(500) 398 | .then((res) => {}); 399 | }); 400 | it('should add it to the histogram', () => { 401 | return supertest(app) 402 | .get('/metrics') 403 | .expect(200) 404 | .then((res) => { 405 | expect(res.text).to.contain('method="GET",route="/v2/v3/bad/:time",code="500"'); 406 | }); 407 | }); 408 | }); 409 | describe('when calling endpoint and getting an error with sub router with two variables', () => { 410 | before(() => { 411 | return supertest(app) 412 | .get('/v2/v3/bad/500/400') 413 | .expect(500) 414 | .then((res) => {}); 415 | }); 416 | it('should add it to the histogram', () => { 417 | return supertest(app) 418 | .get('/metrics') 419 | .expect(200) 420 | .then((res) => { 421 | expect(res.text).to.contain('method="GET",route="/v2/v3/bad/:var1/:var2",code="500"'); 422 | }); 423 | }); 424 | }); 425 | describe('when calling endpoint and getting an error with sub router with no variables', () => { 426 | before(() => { 427 | return supertest(app) 428 | .get('/v2/v3/bad') 429 | .expect(500) 430 | .then((res) => {}); 431 | }); 432 | it('should add it to the histogram', () => { 433 | return supertest(app) 434 | .get('/metrics') 435 | .expect(200) 436 | .then((res) => { 437 | expect(res.text).to.contain('method="GET",route="/v2/v3/bad",code="500"'); 438 | }); 439 | }); 440 | }); 441 | describe('when calling endpoint and getting an error with sub router (root)', () => { 442 | before(() => { 443 | return supertest(app) 444 | .get('/v2/v3') 445 | .expect(500) 446 | .then((res) => {}); 447 | }); 448 | it('should add it to the histogram', () => { 449 | return supertest(app) 450 | .get('/metrics') 451 | .expect(200) 452 | .then((res) => { 453 | expect(res.text).to.contain('method="GET",route="/v2/v3",code="500"'); 454 | }); 455 | }); 456 | }); 457 | describe('when calling endpoint and getting an error from a middleware before sub route', () => { 458 | before(() => { 459 | return supertest(app) 460 | .get('/v2/v3/hello') 461 | .set('error', 'error') 462 | .expect(500) 463 | .then((res) => {}); 464 | }); 465 | it('should add it to the histogram', () => { 466 | return supertest(app) 467 | .get('/metrics') 468 | .expect(200) 469 | .then((res) => { 470 | expect(res.text).to.contain('method="GET",route="/v2/v3",code="500"'); 471 | }); 472 | }); 473 | }); 474 | describe('when calling a GET endpoint with query parmas', () => { 475 | before(() => { 476 | return supertest(app) 477 | .get('/v2/v3?test=test') 478 | .expect(500) 479 | .then((res) => {}); 480 | }); 481 | it('should add it to the histogram', () => { 482 | return supertest(app) 483 | .get('/metrics') 484 | .expect(200) 485 | .then((res) => { 486 | expect(res.text).to.contain('http_request_duration_seconds_bucket{le="+Inf",method="GET",route="/v2/v3",code="500"} 2'); 487 | }); 488 | }); 489 | }); 490 | }); 491 | describe('sub-sub app with error handler in the sub-sub app', function () { 492 | describe('when calling a GET endpoint with path params and sub router', () => { 493 | before(() => { 494 | return supertest(app) 495 | .get('/v2/v4/hello/200') 496 | .expect(200) 497 | .then((res) => {}); 498 | }); 499 | it('should add it to the histogram', () => { 500 | return supertest(app) 501 | .get('/metrics') 502 | .expect(200) 503 | .then((res) => { 504 | expect(res.text).to.contain('http_request_duration_seconds_bucket{le="+Inf",method="GET",route="/v2/v4/hello/:time",code="200"} 1'); 505 | expect(res.text).to.contain('http_response_size_bytes_bucket{le="+Inf",method="GET",route="/v2/v4/hello/:time",code="200"} 1'); 506 | expect(res.text).to.contain('http_request_size_bytes_bucket{le="+Inf",method="GET",route="/v2/v4/hello/:time",code="200"} 1'); 507 | }); 508 | }); 509 | }); 510 | describe('when calling a POST endpoint with sub router', () => { 511 | before(() => { 512 | return supertest(app) 513 | .post('/v2/v4/test') 514 | .send({ name: 'john' }) 515 | .set('Accept', 'application/json') 516 | .expect(201) 517 | .then((res) => {}); 518 | }); 519 | it('should add it to the histogram', () => { 520 | return supertest(app) 521 | .get('/metrics') 522 | .expect(200) 523 | .then((res) => { 524 | expect(res.text).to.contain('method="POST",route="/v2/v4/test",code="201"'); 525 | }); 526 | }); 527 | }); 528 | describe('when calling endpoint and getting an error with sub router only variables', () => { 529 | before(() => { 530 | return supertest(app) 531 | .patch('/v2/v4/500') 532 | .expect(500) 533 | .then((res) => {}); 534 | }); 535 | it('should add it to the histogram', () => { 536 | return supertest(app) 537 | .get('/metrics') 538 | .expect(200) 539 | .then((res) => { 540 | expect(res.text).to.contain('method="PATCH",route="/v2/v4/:time",code="500"'); 541 | }); 542 | }); 543 | }); 544 | describe('when calling endpoint and getting an error with sub router with 1 variable', () => { 545 | before(() => { 546 | return supertest(app) 547 | .get('/v2/v4/bad/500') 548 | .expect(500) 549 | .then((res) => {}); 550 | }); 551 | it('should add it to the histogram', () => { 552 | return supertest(app) 553 | .get('/metrics') 554 | .expect(200) 555 | .then((res) => { 556 | expect(res.text).to.contain('method="GET",route="/v2/v4/bad/:time",code="500"'); 557 | }); 558 | }); 559 | }); 560 | describe('when calling endpoint and getting an error with sub router with two variables', () => { 561 | before(() => { 562 | return supertest(app) 563 | .get('/v2/v4/bad/500/400') 564 | .expect(500) 565 | .then((res) => {}); 566 | }); 567 | it('should add it to the histogram', () => { 568 | return supertest(app) 569 | .get('/metrics') 570 | .expect(200) 571 | .then((res) => { 572 | expect(res.text).to.contain('method="GET",route="/v2/v4/bad/:var1/:var2",code="500"'); 573 | }); 574 | }); 575 | }); 576 | describe('when calling endpoint and getting an error with sub router with no variables', () => { 577 | before(() => { 578 | return supertest(app) 579 | .get('/v2/v4/bad') 580 | .expect(500) 581 | .then((res) => {}); 582 | }); 583 | it('should add it to the histogram', () => { 584 | return supertest(app) 585 | .get('/metrics') 586 | .expect(200) 587 | .then((res) => { 588 | expect(res.text).to.contain('method="GET",route="/v2/v4/bad",code="500"'); 589 | }); 590 | }); 591 | }); 592 | describe('when calling endpoint and getting an error with sub router (root)', () => { 593 | before(() => { 594 | return supertest(app) 595 | .get('/v2/v4') 596 | .expect(500) 597 | .then((res) => {}); 598 | }); 599 | it('should add it to the histogram', () => { 600 | return supertest(app) 601 | .get('/metrics') 602 | .expect(200) 603 | .then((res) => { 604 | expect(res.text).to.contain('method="GET",route="/v2/v4",code="500"'); 605 | }); 606 | }); 607 | }); 608 | describe('when calling endpoint and getting an error from a middleware before sub route', () => { 609 | before(() => { 610 | return supertest(app) 611 | .get('/v2/v4/hello') 612 | .set('error', 'error') 613 | .expect(500) 614 | .then((res) => {}); 615 | }); 616 | it('should add it to the histogram', () => { 617 | return supertest(app) 618 | .get('/metrics') 619 | .expect(200) 620 | .then((res) => { 621 | expect(res.text).to.contain('method="GET",route="/v2/v4",code="500"'); 622 | }); 623 | }); 624 | }); 625 | describe('when calling a GET endpoint with query parmas', () => { 626 | before(() => { 627 | return supertest(app) 628 | .get('/v2/v4?test=test') 629 | .expect(500) 630 | .then((res) => {}); 631 | }); 632 | it('should add it to the histogram', () => { 633 | return supertest(app) 634 | .get('/metrics') 635 | .expect(200) 636 | .then((res) => { 637 | expect(res.text).to.contain('http_request_duration_seconds_bucket{le="+Inf",method="GET",route="/v2/v4",code="500"} 2'); 638 | }); 639 | }); 640 | }); 641 | }); 642 | describe('when calling endpoint and getting an error from a middleware before route', () => { 643 | before(() => { 644 | return supertest(app) 645 | .get('/hello') 646 | .set('error', 'error') 647 | .expect(500) 648 | .then((res) => {}); 649 | }); 650 | it('should add it to the histogram', () => { 651 | return supertest(app) 652 | .get('/metrics') 653 | .expect(200) 654 | .then((res) => { 655 | expect(res.text).to.contain('method="GET",route="N/A",code="500"'); 656 | }); 657 | }); 658 | }); 659 | describe('when using custom metrics', () => { 660 | before(() => { 661 | return supertest(app) 662 | .get('/checkout') 663 | .expect(200) 664 | .then((res) => {}); 665 | }); 666 | it('should add it to the histogram', () => { 667 | return supertest(app) 668 | .get('/metrics') 669 | .expect(200) 670 | .then((res) => { 671 | expect(res.text).to.contain('checkouts_total'); 672 | }); 673 | }); 674 | }); 675 | describe('when calling not existing endpoint', function() { 676 | before(() => { 677 | const notExistingPath = '/notExistingPath' + Math.floor(Math.random() * 10); 678 | return supertest(app) 679 | .get(notExistingPath) 680 | .expect(404) 681 | .then((res) => {}); 682 | }); 683 | it('should add it to the histogram', () => { 684 | return supertest(app) 685 | .get('/metrics') 686 | .expect(200) 687 | .then((res) => { 688 | expect(res.text).to.contain('method="GET",route="N/A",code="404"'); 689 | }); 690 | }); 691 | }); 692 | it('should get metrics as json', () => { 693 | return supertest(app) 694 | .get('/metrics.json') 695 | .expect(200) 696 | .then((res) => { 697 | JSON.parse(res.text); 698 | }); 699 | }); 700 | }); 701 | }); 702 | -------------------------------------------------------------------------------- /test/integration-tests/express/middleware-unique-metrics-name-test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const Prometheus = require('prom-client'); 3 | const expect = require('chai').expect; 4 | const supertest = require('supertest'); 5 | let app, config; 6 | 7 | describe('when using express framework with unique metric names', () => { 8 | before(() => { 9 | config = require('./server/config'); 10 | config.useUniqueHistogramName = true; 11 | app = require('./server/express-server'); 12 | }); 13 | after(() => { 14 | Prometheus.register.clear(); 15 | delete require.cache[require.resolve('./server/express-server')]; 16 | delete require.cache[require.resolve('../../../src/index.js')]; 17 | delete require.cache[require.resolve('../../../src/metrics-middleware.js')]; 18 | }); 19 | describe('when start up with unique metric names', () => { 20 | it('should populate default metrics', () => { 21 | return supertest(app) 22 | .get('/metrics') 23 | .expect(200) 24 | .then((res) => { 25 | expect(res.text).to.contain('express_test_process_cpu_user_seconds_total'); 26 | expect(res.text).to.contain('express_test_process_cpu_system_seconds_total'); 27 | expect(res.text).to.contain('express_test_process_cpu_seconds_total'); 28 | expect(res.text).to.contain('express_test_process_start_time_seconds'); 29 | expect(res.text).to.contain('express_test_process_resident_memory_bytes'); 30 | expect(res.text).to.contain('express_test_nodejs_eventloop_lag_seconds'); 31 | 32 | expect(res.text).to.contain('express_test_nodejs_active_handles_total'); 33 | expect(res.text).to.contain('express_test_nodejs_active_requests_total'); 34 | 35 | expect(res.text).to.contain('express_test_nodejs_heap_size_total_bytes'); 36 | expect(res.text).to.contain('express_test_nodejs_heap_size_used_bytes'); 37 | expect(res.text).to.contain('express_test_nodejs_external_memory_bytes'); 38 | 39 | expect(res.text).to.contain('express_test_nodejs_heap_space_size_total_bytes{space="new"}'); 40 | expect(res.text).to.contain('express_test_nodejs_heap_space_size_total_bytes{space="old"}'); 41 | expect(res.text).to.contain('express_test_nodejs_heap_space_size_total_bytes{space="code"}'); 42 | expect(res.text).to.contain('express_test_nodejs_heap_space_size_total_bytes{space="large_object"}'); 43 | 44 | expect(res.text).to.contain('express_test_nodejs_heap_space_size_used_bytes{space="new"}'); 45 | expect(res.text).to.contain('express_test_nodejs_heap_space_size_used_bytes{space="old"}'); 46 | expect(res.text).to.contain('express_test_nodejs_heap_space_size_used_bytes{space="code"}'); 47 | expect(res.text).to.contain('express_test_nodejs_heap_space_size_used_bytes{space="large_object"}'); 48 | 49 | expect(res.text).to.contain('express_test_nodejs_heap_space_size_available_bytes{space="new"}'); 50 | expect(res.text).to.contain('express_test_nodejs_heap_space_size_available_bytes{space="old"}'); 51 | expect(res.text).to.contain('express_test_nodejs_heap_space_size_available_bytes{space="code"}'); 52 | expect(res.text).to.contain('express_test_nodejs_heap_space_size_available_bytes{space="large_object"}'); 53 | 54 | expect(res.text).to.contain('express_test_nodejs_version_info'); 55 | expect(res.text).to.contain('express_test_app_version{version="1.0.0",major="1",minor="0",patch="0"}'); 56 | }); 57 | }); 58 | describe('when calling a GET endpoint', () => { 59 | before(() => { 60 | return supertest(app) 61 | .get('/hello') 62 | .expect(200) 63 | .then((res) => {}); 64 | }); 65 | it('should add it to the histogram', () => { 66 | return supertest(app) 67 | .get('/metrics') 68 | .expect(200) 69 | .then((res) => { 70 | expect(res.text).to.contain('method="GET",route="/hello",code="200"'); 71 | expect(res.text).to.contain('express_test'); 72 | }); 73 | }); 74 | }); 75 | }); 76 | }); 77 | -------------------------------------------------------------------------------- /test/integration-tests/express/server/config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | useUniqueHistogramName: false 3 | }; -------------------------------------------------------------------------------- /test/integration-tests/express/server/express-server-exclude-routes.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const express = require('express'); 4 | const bodyParser = require('body-parser'); 5 | const app = express(); 6 | const middleware = require('../../../../src/index.js').expressMiddleware; 7 | 8 | app.use(middleware({ excludeRoutes: ['/health', '/health/:id'], includeQueryParams: true })); 9 | app.use(bodyParser.json()); 10 | 11 | app.get('', (req, res, next) => { 12 | setTimeout(() => { 13 | res.json({ message: 'Hello World!' }); 14 | next(); 15 | }, Math.round(Math.random() * 200)); 16 | }); 17 | 18 | app.get('/hello', (req, res, next) => { 19 | setTimeout(() => { 20 | res.json({ message: 'Hello World!' }); 21 | next(); 22 | }, Math.round(Math.random() * 200)); 23 | }); 24 | 25 | app.get('/health', (req, res, next) => { 26 | setTimeout(() => { 27 | res.status(200); 28 | res.json({ message: 'Hello World!' }); 29 | next(); 30 | }, req.body.delay); 31 | }); 32 | 33 | app.get('/health/:id', (req, res, next) => { 34 | setTimeout(() => { 35 | res.status(200); 36 | res.json({ message: 'Hello World!' }); 37 | next(); 38 | }, req.body.delay); 39 | }); 40 | 41 | // Error handler 42 | app.use((err, req, res, next) => { 43 | res.statusCode = 500; 44 | // Do not expose your error in production 45 | res.json({ error: err.message }); 46 | }); 47 | 48 | module.exports = app; 49 | -------------------------------------------------------------------------------- /test/integration-tests/express/server/express-server.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const express = require('express'); 4 | const Prometheus = require('prom-client'); 5 | const bodyParser = require('body-parser'); 6 | const config = require('./config'); 7 | const app = express(); 8 | const middleware = require('../../../../src/index.js').expressMiddleware; 9 | const router = require('./router'); 10 | 11 | const checkoutsTotal = Prometheus.register.getSingleMetric('checkouts_total') || new Prometheus.Counter({ 12 | name: 'checkouts_total', 13 | help: 'Total number of checkouts', 14 | labelNames: ['payment_method'] 15 | }); 16 | 17 | app.use(middleware({ useUniqueHistogramName: config.useUniqueHistogramName })); 18 | app.use(bodyParser.json()); 19 | app.use((req, res, next) => { 20 | if (req.headers.error) { 21 | next(new Error('Error')); 22 | } 23 | next(); 24 | }); 25 | app.use('/v2', router); 26 | 27 | app.get('/hello', (req, res, next) => { 28 | setTimeout(() => { 29 | res.json({ message: 'Hello World!' }); 30 | next(); 31 | }, Math.round(Math.random() * 200)); 32 | }); 33 | 34 | app.get('/hello/:time', (req, res, next) => { 35 | setTimeout(() => { 36 | res.json({ message: 'Hello World!' }); 37 | next(); 38 | }, parseInt(req.params.time)); 39 | }); 40 | 41 | app.get('/bad', (req, res, next) => { 42 | next(new Error('My Error')); 43 | }); 44 | 45 | app.get('/checkout', (req, res, next) => { 46 | const paymentMethod = Math.round(Math.random()) === 0 ? 'stripe' : 'paypal'; 47 | 48 | checkoutsTotal.inc({ 49 | payment_method: paymentMethod 50 | }); 51 | 52 | res.json({ status: 'ok' }); 53 | next(); 54 | }); 55 | 56 | app.post('/test', (req, res, next) => { 57 | setTimeout(() => { 58 | res.status(201); 59 | res.json({ message: 'Hello World!' }); 60 | next(); 61 | }, req.body.delay); 62 | }); 63 | 64 | // Error handler 65 | app.use((err, req, res, next) => { 66 | res.statusCode = 500; 67 | // Do not expose your error in production 68 | res.json({ error: err.message }); 69 | }); 70 | 71 | module.exports = app; 72 | -------------------------------------------------------------------------------- /test/integration-tests/express/server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "express-test", 3 | "version": "1.0.0", 4 | "description": "express test service", 5 | "main": "express-server.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "Idan Tovi", 10 | "license": "ISC" 11 | } 12 | -------------------------------------------------------------------------------- /test/integration-tests/express/server/router.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var express = require('express'); 3 | var router = express.Router(); 4 | const subRouter = require('./sub-router'); 5 | router.use((req, res, next) => { 6 | if (req.headers.error) { 7 | next(new Error('Error')); 8 | } 9 | next(); 10 | }); 11 | 12 | router.use('/v3', subRouter, errorHandler); 13 | router.use('/v4', subRouter); 14 | router.route('/').get(bad); 15 | router.route('/bad').get(bad); 16 | router.route('/bad/:time').get(bad); 17 | router.route('/bad/:var1/:var2').get(bad); 18 | router.route('/test').post(test); 19 | router.route('/:time').patch(bad); 20 | router.route('/hello/:time').get(helloTime); 21 | router.route('/error/:var1').get(bad, errorHandler); 22 | 23 | function test (req, res, next) { 24 | setTimeout(() => { 25 | res.status(201); 26 | res.json({ message: 'Hello World!' }); 27 | next(); 28 | }, req.body.delay); 29 | }; 30 | 31 | function helloTime (req, res, next) { 32 | setTimeout(() => { 33 | res.json({ message: 'Hello World!' }); 34 | next(); 35 | }, parseInt(req.params.time)); 36 | }; 37 | 38 | function bad (req, res, next) { 39 | next(new Error('My Error')); 40 | }; 41 | 42 | // Error handler 43 | function errorHandler(err, req, res, next) { 44 | res.statusCode = 500; 45 | // Do not expose your error in production 46 | res.json({ error: err.message }); 47 | } 48 | 49 | module.exports = router; -------------------------------------------------------------------------------- /test/integration-tests/express/server/sub-router.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var express = require('express'); 3 | var router = express.Router(); 4 | 5 | router.use((req, res, next) => { 6 | if (req.headers.error) { 7 | next(new Error('Error')); 8 | } 9 | next(); 10 | }); 11 | 12 | router.route('/').get(bad); 13 | router.route('/bad').get(bad); 14 | router.route('/bad/:time').get(bad); 15 | router.route('/bad/:var1/:var2').get(bad); 16 | router.route('/test').post(test); 17 | router.route('/:time').patch(bad); 18 | router.route('/hello/:time').get(helloTime); 19 | 20 | function test (req, res, next) { 21 | setTimeout(() => { 22 | res.status(201); 23 | res.json({ message: 'Hello World!' }); 24 | next(); 25 | }, req.body.delay); 26 | } 27 | 28 | function helloTime (req, res, next) { 29 | setTimeout(() => { 30 | res.json({ message: 'Hello World!' }); 31 | next(); 32 | }, parseInt(req.params.time)); 33 | } 34 | 35 | function bad (req, res, next) { 36 | next(new Error('My Error')); 37 | } 38 | 39 | module.exports = router; -------------------------------------------------------------------------------- /test/integration-tests/koa/middleware-exclude-routes-test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const Prometheus = require('prom-client'); 3 | const expect = require('chai').expect; 4 | const supertest = require('supertest'); 5 | let app, config; 6 | 7 | describe('when using koa framework (exclude routes)', () => { 8 | before(() => { 9 | Prometheus.register.clear(); 10 | config = require('./server/config'); 11 | config.useUniqueHistogramName = true; 12 | app = require('./server/koa-server-exclude-routes'); 13 | app = app.listen(3000); 14 | }); 15 | after(() => { 16 | app.close(); 17 | Prometheus.register.clear(); 18 | 19 | delete require.cache[require.resolve('./server/koa-server-exclude-routes.js')]; 20 | delete require.cache[require.resolve('../../../src/index.js')]; 21 | delete require.cache[require.resolve('../../../src/metrics-middleware.js')]; 22 | }); 23 | describe('when start up with exclude route', () => { 24 | describe('when calling a GET endpoint', () => { 25 | before(() => { 26 | return supertest(app) 27 | .get('/hello') 28 | .expect(200) 29 | .then((res) => {}); 30 | }); 31 | it('should add it to the histogram', () => { 32 | return supertest(app) 33 | .get('/metrics') 34 | .expect(200) 35 | .then((res) => { 36 | expect(res.text).to.contain('method="GET",route="/hello",code="200"'); 37 | }); 38 | }); 39 | }); 40 | describe('when calling a GET endpoint of excluded path', () => { 41 | before(() => { 42 | return supertest(app) 43 | .get('/health') 44 | .expect(200) 45 | .then((res) => {}); 46 | }); 47 | it('should add it to the histogram', () => { 48 | return supertest(app) 49 | .get('/metrics') 50 | .expect(200) 51 | .then((res) => { 52 | expect(res.text).to.not.contain('method="GET",route="/health",code="200"'); 53 | }); 54 | }); 55 | }); 56 | describe('when calling a GET endpoint of excluded path with variables', () => { 57 | before(() => { 58 | return supertest(app) 59 | .get('/health/1234') 60 | .expect(200) 61 | .then((res) => {}); 62 | }); 63 | it('should add it to the histogram', () => { 64 | return supertest(app) 65 | .get('/metrics') 66 | .expect(200) 67 | .then((res) => { 68 | expect(res.text).to.not.contain('method="GET",route="/health/:id",code="200"'); 69 | }); 70 | }); 71 | }); 72 | }); 73 | describe('when start up with include query params', () => { 74 | describe('when calling a GET endpoint with one query param', () => { 75 | before(() => { 76 | return supertest(app) 77 | .get('/hello?test=test') 78 | .expect(200) 79 | .then((res) => {}); 80 | }); 81 | it('should add it to the histogram', () => { 82 | return supertest(app) 83 | .get('/metrics') 84 | .expect(200) 85 | .then((res) => { 86 | expect(res.text).to.contain('method="GET",route="/hello?test=",code="200"'); 87 | }); 88 | }); 89 | }); 90 | describe('when calling a GET endpoint with two query params', () => { 91 | before(() => { 92 | return supertest(app) 93 | .get('/hello?test1=test&test2=test2') 94 | .expect(200) 95 | .then((res) => {}); 96 | }); 97 | it('should add it to the histogram and sort the query params', () => { 98 | return supertest(app) 99 | .get('/metrics') 100 | .expect(200) 101 | .then((res) => { 102 | expect(res.text).to.contain('http_request_duration_seconds_count{method="GET",route="/hello?test1=&test2=",code="200"} 1'); 103 | }); 104 | }); 105 | }); 106 | describe('when calling a GET endpoint with two query params in different order', () => { 107 | before(() => { 108 | return supertest(app) 109 | .get('/hello?test2=test&test1=test2') 110 | .expect(200) 111 | .then((res) => {}); 112 | }); 113 | it('should add it to the histogram and sort the query params', () => { 114 | return supertest(app) 115 | .get('/metrics') 116 | .expect(200) 117 | .then((res) => { 118 | expect(res.text).to.contain('http_request_duration_seconds_count{method="GET",route="/hello?test1=&test2=",code="200"} 2'); 119 | }); 120 | }); 121 | }); 122 | describe('when calling a GET endpoint with query param', () => { 123 | before(() => { 124 | return supertest(app) 125 | .get('/health/1234?test=test') 126 | .expect(200) 127 | .then((res) => {}); 128 | }); 129 | it('should add it to the histogram', () => { 130 | return supertest(app) 131 | .get('/metrics') 132 | .expect(200) 133 | .then((res) => { 134 | expect(res.text).to.not.contain('method="GET",route="/health/:id?test=",code="200"'); 135 | }); 136 | }); 137 | }); 138 | describe('when calling a GET root endpoint with query param ', () => { 139 | before(() => { 140 | return supertest(app) 141 | .get('/?test=test') 142 | .expect(200) 143 | .then((res) => {}); 144 | }); 145 | it('should add it to the histogram', () => { 146 | return supertest(app) 147 | .get('/metrics') 148 | .expect(200) 149 | .then((res) => { 150 | expect(res.text).to.contain('http_request_size_bytes_count{method="GET",route="/?test=",code="200"} 1'); 151 | }); 152 | }); 153 | }); 154 | }); 155 | }); 156 | -------------------------------------------------------------------------------- /test/integration-tests/koa/middleware-test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const Prometheus = require('prom-client'); 3 | const expect = require('chai').expect; 4 | const supertest = require('supertest'); 5 | let app, config; 6 | 7 | describe('when using koa framework', () => { 8 | before(() => { 9 | app = require('./server/koa-server'); 10 | app = app.listen(3000); 11 | config = require('./server/config'); 12 | }); 13 | after(() => { 14 | app.close(); 15 | Prometheus.register.clear(); 16 | 17 | delete require.cache[require.resolve('./server/koa-server')]; 18 | delete require.cache[require.resolve('../../../src/index.js')]; 19 | delete require.cache[require.resolve('../../../src/metrics-middleware.js')]; 20 | }); 21 | describe('when start up', () => { 22 | it('should populate default metrics', () => { 23 | return supertest(app) 24 | .get('/metrics') 25 | .expect(200) 26 | .then((res) => { 27 | expect(res.text).to.contain('process_cpu_user_seconds_total'); 28 | expect(res.text).to.contain('process_cpu_system_seconds_total'); 29 | expect(res.text).to.contain('process_cpu_seconds_total'); 30 | expect(res.text).to.contain('process_start_time_seconds'); 31 | expect(res.text).to.contain('process_resident_memory_bytes'); 32 | expect(res.text).to.contain('nodejs_eventloop_lag_seconds'); 33 | 34 | expect(res.text).to.contain('nodejs_active_handles_total'); 35 | expect(res.text).to.contain('nodejs_active_requests_total'); 36 | 37 | expect(res.text).to.contain('nodejs_heap_size_total_bytes'); 38 | expect(res.text).to.contain('nodejs_heap_size_used_bytes'); 39 | expect(res.text).to.contain('nodejs_external_memory_bytes'); 40 | 41 | expect(res.text).to.contain('nodejs_heap_space_size_total_bytes{space="new"}'); 42 | expect(res.text).to.contain('nodejs_heap_space_size_total_bytes{space="old"}'); 43 | expect(res.text).to.contain('nodejs_heap_space_size_total_bytes{space="code"}'); 44 | expect(res.text).to.contain('nodejs_heap_space_size_total_bytes{space="large_object"}'); 45 | 46 | expect(res.text).to.contain('nodejs_heap_space_size_used_bytes{space="new"}'); 47 | expect(res.text).to.contain('nodejs_heap_space_size_used_bytes{space="old"}'); 48 | expect(res.text).to.contain('nodejs_heap_space_size_used_bytes{space="code"}'); 49 | expect(res.text).to.contain('nodejs_heap_space_size_used_bytes{space="large_object"}'); 50 | 51 | expect(res.text).to.contain('nodejs_heap_space_size_available_bytes{space="new"}'); 52 | expect(res.text).to.contain('nodejs_heap_space_size_available_bytes{space="old"}'); 53 | expect(res.text).to.contain('nodejs_heap_space_size_available_bytes{space="code"}'); 54 | expect(res.text).to.contain('nodejs_heap_space_size_available_bytes{space="large_object"}'); 55 | 56 | expect(res.text).to.contain('nodejs_version_info'); 57 | expect(res.text).to.contain('app_version{version="1.0.0",major="1",minor="0",patch="0"}'); 58 | }); 59 | }); 60 | describe('when calling a GET endpoint', () => { 61 | before(() => { 62 | return supertest(app) 63 | .get('/hello') 64 | .expect(200) 65 | .then((res) => {}); 66 | }); 67 | it('should add it to the histogram', () => { 68 | return supertest(app) 69 | .get('/metrics') 70 | .expect(200) 71 | .then((res) => { 72 | expect(res.text).to.contain('method="GET",route="/hello",code="200"'); 73 | }); 74 | }); 75 | }); 76 | describe('when calling a GET endpoint', () => { 77 | before(() => { 78 | return supertest(app) 79 | .get('/hello') 80 | .expect(200) 81 | .then((res) => {}); 82 | }); 83 | it('should add number of open connections', () => { 84 | return supertest(app) 85 | .get('/metrics') 86 | .expect(200) 87 | .then((res) => { 88 | expect(res.text).to.contain('koajs_number_of_open_connections'); 89 | }); 90 | }); 91 | }); 92 | describe('when calling a GET endpoint with path params', () => { 93 | before(() => { 94 | return supertest(app) 95 | .get('/hello/200') 96 | .expect(200) 97 | .then((res) => {}); 98 | }); 99 | it('should add it to the histogram', () => { 100 | return supertest(app) 101 | .get('/metrics') 102 | .expect(200) 103 | .then((res) => { 104 | expect(res.text).to.contain('http_request_duration_seconds_bucket{le="+Inf",method="GET",route="/hello/:time",code="200"} 1'); 105 | expect(res.text).to.contain('http_response_size_bytes_bucket{le="+Inf",method="GET",route="/hello/:time",code="200"} 1'); 106 | expect(res.text).to.contain('http_request_size_bytes_bucket{le="+Inf",method="GET",route="/hello/:time",code="200"} 1'); 107 | }); 108 | }); 109 | }); 110 | describe('when calling a POST endpoint', () => { 111 | before(() => { 112 | return supertest(app) 113 | .post('/test') 114 | .send({ name: 'john' }) 115 | .set('Accept', 'application/json') 116 | .expect(201) 117 | .then((res) => {}); 118 | }); 119 | it('should add it to the histogram', () => { 120 | return supertest(app) 121 | .get('/metrics') 122 | .expect(200) 123 | .then((res) => { 124 | expect(res.text).to.contain('method="POST",route="/test",code="201"'); 125 | }); 126 | }); 127 | }); 128 | describe('when calling endpoint and getting an error', () => { 129 | before(() => { 130 | return supertest(app) 131 | .get('/bad') 132 | .expect(500) 133 | .then((res) => {}); 134 | }); 135 | it('should add it to the histogram', () => { 136 | return supertest(app) 137 | .get('/metrics') 138 | .expect(200) 139 | .then((res) => { 140 | expect(res.text).to.contain('method="GET",route="/bad",code="500"'); 141 | }); 142 | }); 143 | }); 144 | describe('when calling a GET endpoint with query parmas', () => { 145 | before(() => { 146 | return supertest(app) 147 | .get('/hello?test=test') 148 | .expect(200) 149 | .then((res) => {}); 150 | }); 151 | it('should add it to the histogram', () => { 152 | return supertest(app) 153 | .get('/metrics') 154 | .expect(200) 155 | .then((res) => { 156 | expect(res.text).to.contain('http_request_duration_seconds_bucket{le="+Inf",method="GET",route="/hello",code="200"} 3'); 157 | }); 158 | }); 159 | }); 160 | describe('sub app', function () { 161 | describe('when calling a GET endpoint with path params', () => { 162 | before(() => { 163 | return supertest(app) 164 | .get('/v2/hello/200') 165 | .expect(200) 166 | .then((res) => {}); 167 | }); 168 | it('should add it to the histogram', () => { 169 | return supertest(app) 170 | .get('/metrics') 171 | .expect(200) 172 | .then((res) => { 173 | expect(res.text).to.contain('http_request_duration_seconds_bucket{le="+Inf",method="GET",route="/v2/hello/:time",code="200"} 1'); 174 | expect(res.text).to.contain('http_response_size_bytes_bucket{le="+Inf",method="GET",route="/v2/hello/:time",code="200"} 1'); 175 | expect(res.text).to.contain('http_request_size_bytes_bucket{le="+Inf",method="GET",route="/v2/hello/:time",code="200"} 1'); 176 | }); 177 | }); 178 | }); 179 | describe('when calling a POST endpoint', () => { 180 | before(() => { 181 | return supertest(app) 182 | .post('/v2/test') 183 | .send({ name: 'john' }) 184 | .set('Accept', 'application/json') 185 | .expect(201) 186 | .then((res) => {}); 187 | }); 188 | it('should add it to the histogram', () => { 189 | return supertest(app) 190 | .get('/metrics') 191 | .expect(200) 192 | .then((res) => { 193 | expect(res.text).to.contain('method="POST",route="/v2/test",code="201"'); 194 | }); 195 | }); 196 | }); 197 | describe('when calling endpoint and getting an error only variables', () => { 198 | before(() => { 199 | return supertest(app) 200 | .patch('/v2/500') 201 | .expect(500) 202 | .then((res) => {}); 203 | }); 204 | it('should add it to the histogram', () => { 205 | return supertest(app) 206 | .get('/metrics') 207 | .expect(200) 208 | .then((res) => { 209 | expect(res.text).to.contain('method="PATCH",route="/v2/:time",code="500"'); 210 | }); 211 | }); 212 | }); 213 | describe('when calling endpoint and getting an error with 1 variable', () => { 214 | before(() => { 215 | return supertest(app) 216 | .get('/v2/bad/500') 217 | .expect(500) 218 | .then((res) => {}); 219 | }); 220 | it('should add it to the histogram', () => { 221 | return supertest(app) 222 | .get('/metrics') 223 | .expect(200) 224 | .then((res) => { 225 | expect(res.text).to.contain('method="GET",route="/v2/bad/:time",code="500"'); 226 | }); 227 | }); 228 | }); 229 | describe('when calling endpoint and getting an error with two variables', () => { 230 | before(() => { 231 | return supertest(app) 232 | .get('/v2/bad/500/400') 233 | .expect(500) 234 | .then((res) => {}); 235 | }); 236 | it('should add it to the histogram', () => { 237 | return supertest(app) 238 | .get('/metrics') 239 | .expect(200) 240 | .then((res) => { 241 | expect(res.text).to.contain('method="GET",route="/v2/bad/:var1/:var2",code="500"'); 242 | }); 243 | }); 244 | }); 245 | describe('when calling endpoint and getting an error with no variables', () => { 246 | before(() => { 247 | return supertest(app) 248 | .get('/v2/bad') 249 | .expect(500) 250 | .then((res) => {}); 251 | }); 252 | it('should add it to the histogram', () => { 253 | return supertest(app) 254 | .get('/metrics') 255 | .expect(200) 256 | .then((res) => { 257 | expect(res.text).to.contain('method="GET",route="/v2/bad",code="500"'); 258 | }); 259 | }); 260 | }); 261 | describe('when calling endpoint and getting an error (root)', () => { 262 | before(() => { 263 | return supertest(app) 264 | .get('/v2') 265 | .expect(500) 266 | .then((res) => {}); 267 | }); 268 | it('should add it to the histogram', () => { 269 | return supertest(app) 270 | .get('/metrics') 271 | .expect(200) 272 | .then((res) => { 273 | expect(res.text).to.contain('method="GET",route="/v2",code="500"'); 274 | }); 275 | }); 276 | }); 277 | describe('when calling endpoint and getting an error (error handler in the sub app)', () => { 278 | before(() => { 279 | return supertest(app) 280 | .get('/v2/error/500') 281 | .expect(500) 282 | .then((res) => {}); 283 | }); 284 | it('should add it to the histogram', () => { 285 | return supertest(app) 286 | .get('/metrics') 287 | .expect(200) 288 | .then((res) => { 289 | expect(res.text).to.contain('method="GET",route="/v2/error/:var1",code="500"'); 290 | }); 291 | }); 292 | }); 293 | describe('when calling endpoint and getting an error from a middleware before sub route', () => { 294 | before(() => { 295 | return supertest(app) 296 | .get('/v2/hello') 297 | .set('error', 'error') 298 | .expect(500) 299 | .then((res) => {}); 300 | }); 301 | it('should add it to the histogram', () => { 302 | return supertest(app) 303 | .get('/metrics') 304 | .expect(200) 305 | .then((res) => { 306 | expect(res.text).to.contain('method="GET",route="/v2",code="500"'); 307 | }); 308 | }); 309 | }); 310 | describe('when calling a GET endpoint with query params', () => { 311 | before(() => { 312 | return supertest(app) 313 | .get('/v2?test=test') 314 | .expect(500) 315 | .then((res) => {}); 316 | }); 317 | it('should add it to the histogram', () => { 318 | return supertest(app) 319 | .get('/metrics') 320 | .expect(200) 321 | .then((res) => { 322 | expect(res.text).to.contain('http_request_duration_seconds_bucket{le="+Inf",method="GET",route="/v2",code="500"} 2'); 323 | }); 324 | }); 325 | }); 326 | }); 327 | describe('sub-sub app with error handler in the sub app', function () { 328 | describe('when calling a GET endpoint with path params and sub router', () => { 329 | before(() => { 330 | return supertest(app) 331 | .get('/v2/v3/hello/200/') 332 | .expect(200) 333 | .then((res) => {}); 334 | }); 335 | it('should add it to the histogram', () => { 336 | return supertest(app) 337 | .get('/metrics') 338 | .expect(200) 339 | .then((res) => { 340 | expect(res.text).to.contain('http_request_duration_seconds_bucket{le="+Inf",method="GET",route="/v2/v3/hello/:time",code="200"} 1'); 341 | expect(res.text).to.contain('http_response_size_bytes_bucket{le="+Inf",method="GET",route="/v2/v3/hello/:time",code="200"} 1'); 342 | expect(res.text).to.contain('http_request_size_bytes_bucket{le="+Inf",method="GET",route="/v2/v3/hello/:time",code="200"} 1'); 343 | }); 344 | }); 345 | }); 346 | describe('when calling a POST endpoint with sub router', () => { 347 | before(() => { 348 | return supertest(app) 349 | .post('/v2/v3/test') 350 | .send({ name: 'john' }) 351 | .set('Accept', 'application/json') 352 | .expect(201) 353 | .then((res) => {}); 354 | }); 355 | it('should add it to the histogram', () => { 356 | return supertest(app) 357 | .get('/metrics') 358 | .expect(200) 359 | .then((res) => { 360 | expect(res.text).to.contain('method="POST",route="/v2/v3/test",code="201"'); 361 | }); 362 | }); 363 | }); 364 | describe('when calling endpoint and getting an error with sub router only variables', () => { 365 | before(() => { 366 | return supertest(app) 367 | .patch('/v2/v3/500') 368 | .expect(500) 369 | .then((res) => {}); 370 | }); 371 | it('should add it to the histogram', () => { 372 | return supertest(app) 373 | .get('/metrics') 374 | .expect(200) 375 | .then((res) => { 376 | expect(res.text).to.contain('method="PATCH",route="/v2/v3/:time",code="500"'); 377 | }); 378 | }); 379 | }); 380 | describe('when calling endpoint and getting an error with sub router with 1 variable', () => { 381 | before(() => { 382 | return supertest(app) 383 | .get('/v2/v3/bad/500') 384 | .expect(500) 385 | .then((res) => {}); 386 | }); 387 | it('should add it to the histogram', () => { 388 | return supertest(app) 389 | .get('/metrics') 390 | .expect(200) 391 | .then((res) => { 392 | expect(res.text).to.contain('method="GET",route="/v2/v3/bad/:time",code="500"'); 393 | }); 394 | }); 395 | }); 396 | describe('when calling endpoint and getting an error with sub router with two variables', () => { 397 | before(() => { 398 | return supertest(app) 399 | .get('/v2/v3/bad/500/400') 400 | .expect(500) 401 | .then((res) => {}); 402 | }); 403 | it('should add it to the histogram', () => { 404 | return supertest(app) 405 | .get('/metrics') 406 | .expect(200) 407 | .then((res) => { 408 | expect(res.text).to.contain('method="GET",route="/v2/v3/bad/:var1/:var2",code="500"'); 409 | }); 410 | }); 411 | }); 412 | describe('when calling endpoint and getting an error with sub router with no variables', () => { 413 | before(() => { 414 | return supertest(app) 415 | .get('/v2/v3/bad') 416 | .expect(500) 417 | .then((res) => {}); 418 | }); 419 | it('should add it to the histogram', () => { 420 | return supertest(app) 421 | .get('/metrics') 422 | .expect(200) 423 | .then((res) => { 424 | expect(res.text).to.contain('method="GET",route="/v2/v3/bad",code="500"'); 425 | }); 426 | }); 427 | }); 428 | describe('when calling endpoint and getting an error with sub router (root)', () => { 429 | before(() => { 430 | return supertest(app) 431 | .get('/v2/v3') 432 | .expect(500) 433 | .then((res) => {}); 434 | }); 435 | it('should add it to the histogram', () => { 436 | return supertest(app) 437 | .get('/metrics') 438 | .expect(200) 439 | .then((res) => { 440 | expect(res.text).to.contain('method="GET",route="/v2/v3",code="500"'); 441 | }); 442 | }); 443 | }); 444 | describe('when calling endpoint and getting an error from a middleware before sub route', () => { 445 | before(() => { 446 | return supertest(app) 447 | .get('/v2/v3/hello') 448 | .set('error', 'error') 449 | .expect(500) 450 | .then((res) => {}); 451 | }); 452 | it('should add it to the histogram', () => { 453 | return supertest(app) 454 | .get('/metrics') 455 | .expect(200) 456 | .then((res) => { 457 | expect(res.text).to.contain('method="GET",route="/v2/v3",code="500"'); 458 | }); 459 | }); 460 | }); 461 | describe('when calling a GET endpoint with query parmas', () => { 462 | before(() => { 463 | return supertest(app) 464 | .get('/v2/v3?test=test') 465 | .expect(500) 466 | .then((res) => {}); 467 | }); 468 | it('should add it to the histogram', () => { 469 | return supertest(app) 470 | .get('/metrics') 471 | .expect(200) 472 | .then((res) => { 473 | expect(res.text).to.contain('http_request_duration_seconds_bucket{le="+Inf",method="GET",route="/v2/v3",code="500"} 2'); 474 | }); 475 | }); 476 | }); 477 | }); 478 | describe('sub-sub app with error handler in the sub-sub app', function () { 479 | describe('when calling a GET endpoint with path params and sub router', () => { 480 | before(() => { 481 | return supertest(app) 482 | .get('/v2/v4/hello/200') 483 | .expect(200) 484 | .then((res) => {}); 485 | }); 486 | it('should add it to the histogram', () => { 487 | return supertest(app) 488 | .get('/metrics') 489 | .expect(200) 490 | .then((res) => { 491 | expect(res.text).to.contain('http_request_duration_seconds_bucket{le="+Inf",method="GET",route="/v2/v4/hello/:time",code="200"} 1'); 492 | expect(res.text).to.contain('http_response_size_bytes_bucket{le="+Inf",method="GET",route="/v2/v4/hello/:time",code="200"} 1'); 493 | expect(res.text).to.contain('http_request_size_bytes_bucket{le="+Inf",method="GET",route="/v2/v4/hello/:time",code="200"} 1'); 494 | }); 495 | }); 496 | }); 497 | describe('when calling a POST endpoint with sub router', () => { 498 | before(() => { 499 | return supertest(app) 500 | .post('/v2/v4/test') 501 | .send({ name: 'john' }) 502 | .set('Accept', 'application/json') 503 | .expect(201) 504 | .then((res) => {}); 505 | }); 506 | it('should add it to the histogram', () => { 507 | return supertest(app) 508 | .get('/metrics') 509 | .expect(200) 510 | .then((res) => { 511 | expect(res.text).to.contain('method="POST",route="/v2/v4/test",code="201"'); 512 | }); 513 | }); 514 | }); 515 | describe('when calling endpoint and getting an error with sub router only variables', () => { 516 | before(() => { 517 | return supertest(app) 518 | .patch('/v2/v4/500') 519 | .expect(500) 520 | .then((res) => {}); 521 | }); 522 | it('should add it to the histogram', () => { 523 | return supertest(app) 524 | .get('/metrics') 525 | .expect(200) 526 | .then((res) => { 527 | expect(res.text).to.contain('method="PATCH",route="/v2/v4/:time",code="500"'); 528 | }); 529 | }); 530 | }); 531 | describe('when calling endpoint and getting an error with sub router with 1 variable', () => { 532 | before(() => { 533 | return supertest(app) 534 | .get('/v2/v4/bad/500') 535 | .expect(500) 536 | .then((res) => {}); 537 | }); 538 | it('should add it to the histogram', () => { 539 | return supertest(app) 540 | .get('/metrics') 541 | .expect(200) 542 | .then((res) => { 543 | expect(res.text).to.contain('method="GET",route="/v2/v4/bad/:time",code="500"'); 544 | }); 545 | }); 546 | }); 547 | describe('when calling endpoint and getting an error with sub router with two variables', () => { 548 | before(() => { 549 | return supertest(app) 550 | .get('/v2/v4/bad/500/400') 551 | .expect(500) 552 | .then((res) => {}); 553 | }); 554 | it('should add it to the histogram', () => { 555 | return supertest(app) 556 | .get('/metrics') 557 | .expect(200) 558 | .then((res) => { 559 | expect(res.text).to.contain('method="GET",route="/v2/v4/bad/:var1/:var2",code="500"'); 560 | }); 561 | }); 562 | }); 563 | describe('when calling endpoint and getting an error with sub router with no variables', () => { 564 | before(() => { 565 | return supertest(app) 566 | .get('/v2/v4/bad') 567 | .expect(500) 568 | .then((res) => {}); 569 | }); 570 | it('should add it to the histogram', () => { 571 | return supertest(app) 572 | .get('/metrics') 573 | .expect(200) 574 | .then((res) => { 575 | expect(res.text).to.contain('method="GET",route="/v2/v4/bad",code="500"'); 576 | }); 577 | }); 578 | }); 579 | describe('when calling endpoint and getting an error with sub router (root)', () => { 580 | before(() => { 581 | return supertest(app) 582 | .get('/v2/v4') 583 | .expect(500) 584 | .then((res) => {}); 585 | }); 586 | it('should add it to the histogram', () => { 587 | return supertest(app) 588 | .get('/metrics') 589 | .expect(200) 590 | .then((res) => { 591 | expect(res.text).to.contain('method="GET",route="/v2/v4",code="500"'); 592 | }); 593 | }); 594 | }); 595 | describe('when calling endpoint and getting an error from a middleware before sub route', () => { 596 | before(() => { 597 | return supertest(app) 598 | .get('/v2/v4/hello') 599 | .set('error', 'error') 600 | .expect(500) 601 | .then((res) => {}); 602 | }); 603 | it('should add it to the histogram', () => { 604 | return supertest(app) 605 | .get('/metrics') 606 | .expect(200) 607 | .then((res) => { 608 | expect(res.text).to.contain('method="GET",route="/v2/v4",code="500"'); 609 | }); 610 | }); 611 | }); 612 | describe('when calling a GET endpoint with query parmas', () => { 613 | before(() => { 614 | return supertest(app) 615 | .get('/v2/v4?test=test') 616 | .expect(500) 617 | .then((res) => {}); 618 | }); 619 | it('should add it to the histogram', () => { 620 | return supertest(app) 621 | .get('/metrics') 622 | .expect(200) 623 | .then((res) => { 624 | expect(res.text).to.contain('http_request_duration_seconds_bucket{le="+Inf",method="GET",route="/v2/v4",code="500"} 2'); 625 | }); 626 | }); 627 | }); 628 | }); 629 | describe('when calling endpoint and getting an error from a middleware before route', () => { 630 | before(() => { 631 | return supertest(app) 632 | .get('/hello') 633 | .set('error', 'error') 634 | .expect(500) 635 | .then((res) => {}); 636 | }); 637 | it('should add it to the histogram', () => { 638 | return supertest(app) 639 | .get('/metrics') 640 | .expect(200) 641 | .then((res) => { 642 | expect(res.text).to.contain('method="GET",route="N/A",code="500"'); 643 | }); 644 | }); 645 | }); 646 | describe('when using custom metrics', () => { 647 | before(() => { 648 | return supertest(app) 649 | .get('/checkout') 650 | .expect(200) 651 | .then((res) => {}); 652 | }); 653 | it('should add it to the histogram', () => { 654 | return supertest(app) 655 | .get('/metrics') 656 | .expect(200) 657 | .then((res) => { 658 | expect(res.text).to.contain('checkouts_total'); 659 | }); 660 | }); 661 | }); 662 | describe('when calling not existing endpoint', function() { 663 | before(() => { 664 | const notExistingPath = '/notExistingPath' + Math.floor(Math.random() * 10); 665 | return supertest(app) 666 | .get(notExistingPath) 667 | .expect(404) 668 | .then((res) => {}); 669 | }); 670 | it('should add it to the histogram', () => { 671 | return supertest(app) 672 | .get('/metrics') 673 | .expect(200) 674 | .then((res) => { 675 | expect(res.text).to.contain('method="GET",route="N/A",code="404"'); 676 | }); 677 | }); 678 | }); 679 | describe('when calling wildcard endpoint', function () { 680 | before(() => { 681 | const wildcardPath = '/wild-path/' + Math.floor(Math.random() * 10); 682 | return supertest(app) 683 | .get(wildcardPath) 684 | .expect(200) 685 | .then((res) => {}); 686 | }); 687 | it('should add it to the histogram', () => { 688 | return supertest(app) 689 | .get('/metrics') 690 | .expect(200) 691 | .then((res) => { 692 | expect(res.text).to.contain('method="GET",route="N/A",code="200"'); 693 | }); 694 | }); 695 | }); 696 | it('should get metrics as json', () => { 697 | return supertest(app) 698 | .get('/metrics.json') 699 | .expect(200) 700 | .then((res) => { 701 | JSON.parse(res.text); 702 | }); 703 | }); 704 | }); 705 | }); 706 | -------------------------------------------------------------------------------- /test/integration-tests/koa/middleware-unique-metrics-name-test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const Prometheus = require('prom-client'); 3 | const expect = require('chai').expect; 4 | const supertest = require('supertest'); 5 | let app, config; 6 | 7 | describe('when using koa framework with unique metric names', () => { 8 | before(() => { 9 | config = require('./server/config'); 10 | config.useUniqueHistogramName = true; 11 | app = require('./server/koa-server'); 12 | app = app.listen(3000); 13 | }); 14 | after(() => { 15 | app.close(); 16 | Prometheus.register.clear(); 17 | 18 | delete require.cache[require.resolve('./server/koa-server')]; 19 | delete require.cache[require.resolve('../../../src/index.js')]; 20 | delete require.cache[require.resolve('../../../src/metrics-middleware.js')]; 21 | }); 22 | describe('when start up with unique metric names', () => { 23 | it('should populate default metrics', () => { 24 | return supertest(app) 25 | .get('/metrics') 26 | .expect(200) 27 | .then((res) => { 28 | expect(res.text).to.contain('koa_test_process_cpu_user_seconds_total'); 29 | expect(res.text).to.contain('koa_test_process_cpu_system_seconds_total'); 30 | expect(res.text).to.contain('koa_test_process_cpu_seconds_total'); 31 | expect(res.text).to.contain('koa_test_process_start_time_seconds'); 32 | expect(res.text).to.contain('koa_test_process_resident_memory_bytes'); 33 | expect(res.text).to.contain('koa_test_nodejs_eventloop_lag_seconds'); 34 | 35 | expect(res.text).to.contain('koa_test_nodejs_active_handles_total'); 36 | expect(res.text).to.contain('koa_test_nodejs_active_requests_total'); 37 | 38 | expect(res.text).to.contain('koa_test_nodejs_heap_size_total_bytes'); 39 | expect(res.text).to.contain('koa_test_nodejs_heap_size_used_bytes'); 40 | expect(res.text).to.contain('koa_test_nodejs_external_memory_bytes'); 41 | 42 | expect(res.text).to.contain('koa_test_nodejs_heap_space_size_total_bytes{space="new"}'); 43 | expect(res.text).to.contain('koa_test_nodejs_heap_space_size_total_bytes{space="old"}'); 44 | expect(res.text).to.contain('koa_test_nodejs_heap_space_size_total_bytes{space="code"}'); 45 | expect(res.text).to.contain('koa_test_nodejs_heap_space_size_total_bytes{space="large_object"}'); 46 | 47 | expect(res.text).to.contain('koa_test_nodejs_heap_space_size_used_bytes{space="new"}'); 48 | expect(res.text).to.contain('koa_test_nodejs_heap_space_size_used_bytes{space="old"}'); 49 | expect(res.text).to.contain('koa_test_nodejs_heap_space_size_used_bytes{space="code"}'); 50 | expect(res.text).to.contain('koa_test_nodejs_heap_space_size_used_bytes{space="large_object"}'); 51 | 52 | expect(res.text).to.contain('koa_test_nodejs_heap_space_size_available_bytes{space="new"}'); 53 | expect(res.text).to.contain('koa_test_nodejs_heap_space_size_available_bytes{space="old"}'); 54 | expect(res.text).to.contain('koa_test_nodejs_heap_space_size_available_bytes{space="code"}'); 55 | expect(res.text).to.contain('koa_test_nodejs_heap_space_size_available_bytes{space="large_object"}'); 56 | 57 | expect(res.text).to.contain('koa_test_nodejs_version_info'); 58 | expect(res.text).to.contain('koa_test_app_version{version="1.0.0",major="1",minor="0",patch="0"}'); 59 | }); 60 | }); 61 | describe('when calling a GET endpoint', () => { 62 | before(() => { 63 | return supertest(app) 64 | .get('/hello') 65 | .expect(200) 66 | .then((res) => {}); 67 | }); 68 | it('should add it to the histogram', () => { 69 | return supertest(app) 70 | .get('/metrics') 71 | .expect(200) 72 | .then((res) => { 73 | expect(res.text).to.contain('method="GET",route="/hello",code="200"'); 74 | expect(res.text).to.contain('koa_test'); 75 | }); 76 | }); 77 | }); 78 | }); 79 | }); 80 | -------------------------------------------------------------------------------- /test/integration-tests/koa/server/config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | useUniqueHistogramName: false 3 | }; -------------------------------------------------------------------------------- /test/integration-tests/koa/server/koa-server-exclude-routes.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Koa = require('koa'); 4 | const bodyParser = require('koa-bodyparser'); 5 | const app = new Koa(); 6 | const middleware = require('../../../../src/index.js').koaMiddleware; 7 | const Router = require('koa-router'); 8 | const router = new Router(); 9 | const sleep = require('../../../utils/sleep'); 10 | 11 | app.use(async (ctx, next) => { 12 | try { 13 | await next(); 14 | } catch (err) { 15 | ctx.status = 500; 16 | // Do not expose your error in production 17 | ctx.body = { error: err.message }; 18 | } 19 | }); 20 | 21 | app.use(middleware({ excludeRoutes: ['/health', '/health/:id'], includeQueryParams: true })); 22 | app.use(bodyParser()); 23 | 24 | app.use(router.routes()); 25 | 26 | router.get('', async (ctx, next) => { 27 | await sleep(Math.round(Math.random() * 200)); 28 | ctx.body = { message: 'Hello World!' }; 29 | ctx.status = 200; 30 | next(); 31 | }); 32 | 33 | router.get('/hello', async (ctx, next) => { 34 | await sleep(Math.round(Math.random() * 200)); 35 | ctx.body = { message: 'Hello World!' }; 36 | ctx.status = 200; 37 | next(); 38 | }); 39 | 40 | router.get('/health', async (ctx, next) => { 41 | await sleep(Number.parseInt(ctx.request.body.delay || 1)); 42 | ctx.body = { message: 'Hello World!' }; 43 | ctx.status = 200; 44 | next(); 45 | }); 46 | 47 | router.get('/health/:id', async (ctx, next) => { 48 | await sleep(Number.parseInt(ctx.request.body.delay || 1)); 49 | ctx.body = { message: 'Hello World!' }; 50 | ctx.status = 200; 51 | next(); 52 | }); 53 | 54 | module.exports = app; 55 | -------------------------------------------------------------------------------- /test/integration-tests/koa/server/koa-server.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Koa = require('koa'); 4 | const Prometheus = require('prom-client'); 5 | const bodyParser = require('koa-bodyparser'); 6 | const config = require('./config'); 7 | const app = new Koa(); 8 | const middleware = require('../../../../src/index.js').koaMiddleware; 9 | const subRouter = require('./router'); 10 | const sleep = require('../../../utils/sleep'); 11 | const Router = require('koa-router'); 12 | const router = new Router(); 13 | 14 | const checkoutsTotal = Prometheus.register.getSingleMetric('checkouts_total') || new Prometheus.Counter({ 15 | name: 'checkouts_total', 16 | help: 'Total number of checkouts', 17 | labelNames: ['payment_method'] 18 | }); 19 | 20 | app.use(bodyParser()); 21 | 22 | // Error handler 23 | app.use(async (ctx, next) => { 24 | try { 25 | await next(); 26 | } catch (err) { 27 | ctx.status = 500; 28 | // Do not expose your error in production 29 | ctx.body = { error: err.message }; 30 | } 31 | }); 32 | 33 | app.use(middleware({ useUniqueHistogramName: config.useUniqueHistogramName })); 34 | 35 | app.use((ctx, next) => { 36 | if (ctx.headers.error) { 37 | throw new Error('Error'); 38 | } 39 | return next(); 40 | }); 41 | 42 | app.use(subRouter.routes()); 43 | app.use(router.routes()); 44 | 45 | router.get('/hello', async (ctx, next) => { 46 | await sleep(Math.round(Math.random() * 200)); 47 | ctx.status = 200; 48 | ctx.body = { message: 'Hello World!' }; 49 | return next(); 50 | }); 51 | 52 | router.get('/hello/:time', async (ctx, next) => { 53 | await sleep(Number.parseInt(ctx.params.time)); 54 | ctx.status = 200; 55 | ctx.body = { message: 'Hello World!' }; 56 | return next(); 57 | }); 58 | 59 | router.get('/bad', (ctx, next) => { 60 | ctx.throw(new Error('My Error')); 61 | }); 62 | 63 | router.get('/checkout', (ctx, next) => { 64 | const paymentMethod = Math.round(Math.random()) === 0 ? 'stripe' : 'paypal'; 65 | 66 | checkoutsTotal.inc({ 67 | payment_method: paymentMethod 68 | }); 69 | 70 | ctx.body = { status: 'ok' }; 71 | next(); 72 | }); 73 | 74 | router.post('/test', async (ctx, next) => { 75 | await sleep(Number.parseInt(ctx.request.body.delay || 1)); 76 | ctx.status = 201; 77 | ctx.body = { message: 'Test World!' }; 78 | return next(); 79 | }); 80 | 81 | router.get('/wild-path/(.*)', (ctx, next) => { 82 | ctx.status = 200; 83 | ctx.body = { message: 'Wildcard route reached!' }; 84 | return next(); 85 | }); 86 | 87 | module.exports = app; 88 | -------------------------------------------------------------------------------- /test/integration-tests/koa/server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "koa-test", 3 | "version": "1.0.0", 4 | "description": "koa test service", 5 | "main": "koa-server.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "Dina Yakovlev", 10 | "license": "ISC" 11 | } 12 | -------------------------------------------------------------------------------- /test/integration-tests/koa/server/router.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const cloneDeep = require('lodash.clonedeep'); 3 | const Router = require('koa-router'); 4 | const router = new Router({ 5 | prefix: '/v2' 6 | }); 7 | const sleep = require('../../../utils/sleep'); 8 | const subRouter = require('./sub-router'); 9 | 10 | router.use('/v3', cloneDeep(subRouter.routes())); // Current koa router has an open issue/bug (https://github.com/alexmingoia/koa-router/issues/244), only way found to mount a shared router at different paths. 11 | router.use('/v4', cloneDeep(subRouter.routes())); 12 | router.get('/', bad); 13 | router.get('/bad', bad); 14 | router.get('/bad/:time', bad); 15 | router.get('/bad/:var1/:var2', bad); 16 | router.post('/test', test); 17 | router.patch('/:time', bad); 18 | router.get('/hello/:time', helloTime); 19 | router.get('/hello/', helloTime); 20 | router.get('/error/:var1', bad); 21 | 22 | async function test (ctx, next) { 23 | await sleep(Number.parseInt(ctx.request.body.delay || 1)); 24 | ctx.status = 201; 25 | ctx.body = { message: 'Hello World!' }; 26 | next(); 27 | }; 28 | 29 | async function helloTime (ctx, next) { 30 | await sleep(Number.parseInt(ctx.params.time)); 31 | ctx.status = 200; 32 | ctx.body = { message: 'Hello World!' }; 33 | next(); 34 | }; 35 | 36 | function bad (ctx) { 37 | throw new Error({ error: 'My Error' }); 38 | }; 39 | 40 | module.exports = router; 41 | -------------------------------------------------------------------------------- /test/integration-tests/koa/server/sub-router.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var Router = require('koa-router'); 3 | var router = new Router(); 4 | const sleep = require('../../../utils/sleep'); 5 | 6 | router.get('/', bad); 7 | router.get('/bad', bad); 8 | router.get('/bad/:time', bad); 9 | router.get('/bad/:var1/:var2', bad); 10 | router.post('/test', test); 11 | router.patch('/:time', bad); 12 | router.get('/hello', bad); 13 | router.get('/hello/:time', helloTime); 14 | 15 | async function test(ctx, next) { 16 | await sleep(Number.parseInt(ctx.request.body.delay || 1)); 17 | 18 | ctx.status = 201; 19 | ctx.body = { message: 'Hello World!' }; 20 | next(); 21 | }; 22 | 23 | async function helloTime (ctx, next) { 24 | await sleep(Number.parseInt(ctx.params.time)); 25 | ctx.status = 200; 26 | ctx.boy = { message: 'Hello World!' }; 27 | next(); 28 | }; 29 | 30 | function bad (ctx, next) { 31 | ctx.throw(500, new Error('My Error')); 32 | }; 33 | 34 | router.use(async (ctx, next) => { 35 | if (ctx.headers.error) { 36 | ctx.throw(500, new Error('My Error')); 37 | } 38 | next(); 39 | }); 40 | 41 | module.exports = router; 42 | -------------------------------------------------------------------------------- /test/integration-tests/nest-js/middleware-test.spec.ts: -------------------------------------------------------------------------------- 1 | import * as express from 'express'; 2 | import * as Prometheus from 'prom-client'; 3 | import * as request from 'supertest'; 4 | import { Test } from '@nestjs/testing'; 5 | import { UsersModule } from "./users.module"; 6 | 7 | const expect = require('chai').expect; 8 | describe('when using nest-js framework', () => { 9 | let server; 10 | before(async () => { 11 | const middleware = require('../../../src/index.js'); 12 | 13 | let module = Test.createTestingModule({imports: [UsersModule]}); 14 | 15 | const moduleRef = await module.compile() 16 | const app = moduleRef.createNestApplication(); 17 | app.use(middleware()); 18 | await app.init(); 19 | server = app.getHttpServer(); 20 | }); 21 | after(() => { 22 | Prometheus.register.clear(); 23 | 24 | delete require.cache[require.resolve('../../../src/index.js')]; 25 | delete require.cache[require.resolve('../../../src/metrics-middleware.js')]; 26 | }); 27 | describe('when calling a POST user/:user_id endpoint with user_id as pattern', () => { 28 | before(() => { 29 | return request(server) 30 | .post('/users/123') 31 | .expect(201) 32 | .expect({ 33 | result: 'success' 34 | }); 35 | }); 36 | it('should add it to the histogram', () => { 37 | return request(server) 38 | .get('/metrics') 39 | .expect(200) 40 | .then((res) => { 41 | expect(res.text).to.contain('method="POST",route="/users/:user_id",code="201"'); 42 | }); 43 | }); 44 | }); 45 | describe('When calling a GET user/:user_id/app-id/:app_id with user_id and app_id as pattern', () => { 46 | before(() => { 47 | return request(server) 48 | .get('/users/123/app-id/456') 49 | .expect(200) 50 | .expect({ 51 | app_id: 'some_app_id' 52 | }); 53 | }); 54 | 55 | it('should add it to the histogram', () => { 56 | return request(server) 57 | .get('/metrics') 58 | .expect(200) 59 | .then((res) => { 60 | expect(res.text).to.contain('method="GET",route="/users/:user_id/app-id/:app_id",code="200"'); 61 | }); 62 | }); 63 | }); 64 | describe('When calling a GET user/:user_id/app-id/:app_id with user_id and app_id as pattern and query params', () => { 65 | before(() => { 66 | return request(server) 67 | .get('/users/123/app-id/456?test=test') 68 | .expect(200) 69 | .expect({ 70 | app_id: 'some_app_id' 71 | }); 72 | }); 73 | 74 | it('should add it to the histogram', () => { 75 | return request(server) 76 | .get('/metrics') 77 | .expect(200) 78 | .then((res) => { 79 | expect(res.text).to.contain('http_response_size_bytes_count{method="GET",route="/users/:user_id/app-id/:app_id",code="200"} 2'); 80 | }); 81 | }); 82 | }); 83 | }); 84 | 85 | describe('when using nest-js framework and includeQueryParams', () => { 86 | let server; 87 | before(async () => { 88 | const middleware = require('../../../src/index.js'); 89 | 90 | const module = Test.createTestingModule({imports: [UsersModule]}); 91 | 92 | const moduleRef = await module.compile(); 93 | const app = moduleRef.createNestApplication(server); 94 | app.use(middleware({ includeQueryParams: true })); 95 | 96 | await app.init(); 97 | server = app.getHttpServer(); 98 | }); 99 | after(() => { 100 | Prometheus.register.clear(); 101 | }); 102 | describe('When calling a GET user/:user_id/app-id/:app_id with user_id and app_id as pattern and query params', () => { 103 | before(() => { 104 | return request(server) 105 | .get('/users/123/app-id/456?test=test') 106 | .expect(200) 107 | .expect({ 108 | app_id: 'some_app_id' 109 | }); 110 | }); 111 | 112 | it('should add it to the histogram', () => { 113 | return request(server) 114 | .get('/metrics') 115 | .expect(200) 116 | .then((res) => { 117 | expect(res.text).to.contain('http_response_size_bytes_count{method="GET",route="/users/:user_id/app-id/:app_id?test=",code="200"} 1'); 118 | }); 119 | }); 120 | }); 121 | describe('When calling a GET user/:user_id/app-id/:app_id with user_id and app_id as pattern and two query params', () => { 122 | before(() => { 123 | return request(server) 124 | .get('/users/123/app-id/456?test=test&test1=test1') 125 | .expect(200) 126 | .expect({ 127 | app_id: 'some_app_id' 128 | }); 129 | }); 130 | 131 | it('should add it to the histogram', () => { 132 | return request(server) 133 | .get('/metrics') 134 | .expect(200) 135 | .then((res) => { 136 | expect(res.text).to.contain('http_response_size_bytes_count{method="GET",route="/users/:user_id/app-id/:app_id?test=&test1=",code="200"} 1'); 137 | }); 138 | }); 139 | }); 140 | describe('When calling a GET user/:user_id/app-id/:app_id with user_id and app_id as pattern and two query params in different order', () => { 141 | before(() => { 142 | return request(server) 143 | .get('/users/123/app-id/456?test1=test1&test=test') 144 | .expect(200) 145 | .expect({ 146 | app_id: 'some_app_id' 147 | }); 148 | }); 149 | 150 | it('should add it to the histogram', () => { 151 | return request(server) 152 | .get('/metrics') 153 | .expect(200) 154 | .then((res) => { 155 | expect(res.text).to.contain('http_response_size_bytes_count{method="GET",route="/users/:user_id/app-id/:app_id?test=&test1=",code="200"} 2'); 156 | }); 157 | }); 158 | }); 159 | }); 160 | -------------------------------------------------------------------------------- /test/integration-tests/nest-js/users.controller.ts: -------------------------------------------------------------------------------- 1 | import {Controller, Get, Post} from '@nestjs/common'; 2 | 3 | @Controller('users/:user_id') 4 | export class UsersController { 5 | 6 | @Post() 7 | async createApp() { 8 | return { 9 | result: 'success' 10 | } 11 | } 12 | 13 | @Get('app-id/:app_id') 14 | async getUser() { 15 | return { 16 | app_id: 'some_app_id' 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /test/integration-tests/nest-js/users.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { UsersController } from './users.controller'; 3 | 4 | @Module({ 5 | controllers: [UsersController] 6 | }) 7 | 8 | export class UsersModule {} -------------------------------------------------------------------------------- /test/unit-test/metric-middleware-test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Prometheus = require('prom-client'); 4 | const sinon = require('sinon'); 5 | const chai = require('chai'); 6 | const chaiAsPromised = require('chai-as-promised'); 7 | const rewire = require('rewire'); 8 | const middleware = rewire('../../src/metrics-middleware')('1.0.0'); 9 | const httpMocks = require('node-mocks-http'); 10 | const EventEmitter = require('events').EventEmitter; 11 | const expect = chai.expect; 12 | chai.use(chaiAsPromised); 13 | 14 | describe('metrics-middleware', () => { 15 | after(() => { 16 | Prometheus.register.clear(); 17 | }); 18 | describe('when calling the function with options', () => { 19 | before(async () => { 20 | await middleware({ 21 | durationBuckets: [1, 10, 50, 100, 300, 500, 1000], 22 | requestSizeBuckets: [0, 1, 5, 10, 15], 23 | responseSizeBuckets: [250, 500, 1000, 2500, 5000, 10000, 15000, 20000] 24 | }); 25 | }); 26 | it('should have http_request_size_bytes metrics with custom buckets', () => { 27 | expect(Prometheus.register.getSingleMetric('http_request_size_bytes').bucketValues).to.have.all.keys([0, 1, 5, 10, 15]); 28 | }); 29 | it('should have http_request_size_bytes with the right labels', () => { 30 | expect(Prometheus.register.getSingleMetric('http_request_size_bytes').labelNames).to.have.members(['method', 'route', 'code']); 31 | }); 32 | it('should have http_request_duration_seconds metrics with custom buckets', () => { 33 | expect(Prometheus.register.getSingleMetric('http_request_duration_seconds').bucketValues).to.have.all.keys([1, 10, 50, 100, 300, 500, 1000]); 34 | }); 35 | it('should have http_request_duration_seconds with the right labels', () => { 36 | expect(Prometheus.register.getSingleMetric('http_request_duration_seconds').labelNames).to.have.members(['method', 'route', 'code']); 37 | }); 38 | it('should have http_response_size_bytes metrics with custom buckets', () => { 39 | expect(Prometheus.register.getSingleMetric('http_response_size_bytes').bucketValues).to.have.all.keys([250, 500, 1000, 2500, 5000, 10000, 15000, 20000]); 40 | }); 41 | it('should have http_response_size_bytes with the right labels', () => { 42 | expect(Prometheus.register.getSingleMetric('http_response_size_bytes').labelNames).to.have.members(['method', 'route', 'code']); 43 | }); 44 | after(() => { 45 | Prometheus.register.clear(); 46 | }); 47 | }); 48 | describe('when calling the function with options (metrics prefix)', () => { 49 | before(async () => { 50 | await middleware({ 51 | durationBuckets: [1, 10, 50, 100, 300, 500, 1000], 52 | requestSizeBuckets: [0, 1, 5, 10, 15], 53 | responseSizeBuckets: [250, 500, 1000, 2500, 5000, 10000, 15000, 20000], 54 | metricsPrefix: 'prefix' 55 | }); 56 | }); 57 | it('should have prefix_http_request_size_bytes metrics with custom buckets', () => { 58 | expect(Prometheus.register.getSingleMetric('prefix_http_request_size_bytes').bucketValues).to.have.all.keys([0, 1, 5, 10, 15]); 59 | }); 60 | it('should have prefix_http_request_size_bytes with the right labels', () => { 61 | expect(Prometheus.register.getSingleMetric('prefix_http_request_size_bytes').labelNames).to.have.members(['method', 'route', 'code']); 62 | }); 63 | it('should have prefix_http_request_duration_seconds metrics with custom buckets', () => { 64 | expect(Prometheus.register.getSingleMetric('prefix_http_request_duration_seconds').bucketValues).to.have.all.keys([1, 10, 50, 100, 300, 500, 1000]); 65 | }); 66 | it('should have prefix_http_request_duration_seconds with the right labels', () => { 67 | expect(Prometheus.register.getSingleMetric('prefix_http_request_duration_seconds').labelNames).to.have.members(['method', 'route', 'code']); 68 | }); 69 | it('should have prefix_http_response_size_bytes metrics with custom buckets', () => { 70 | expect(Prometheus.register.getSingleMetric('prefix_http_response_size_bytes').bucketValues).to.have.all.keys([250, 500, 1000, 2500, 5000, 10000, 15000, 20000]); 71 | }); 72 | it('should have prefix_http_response_size_bytes with the right labels', () => { 73 | expect(Prometheus.register.getSingleMetric('prefix_http_response_size_bytes').labelNames).to.have.members(['method', 'route', 'code']); 74 | }); 75 | it('should have default metrics with prefix', () => { 76 | expect(Prometheus.register.getSingleMetric('prefix_process_cpu_user_seconds_total')).to.exist; 77 | expect(Prometheus.register.getSingleMetric('prefix_process_cpu_system_seconds_total')).to.exist; 78 | expect(Prometheus.register.getSingleMetric('prefix_process_cpu_seconds_total')).to.exist; 79 | expect(Prometheus.register.getSingleMetric('prefix_process_start_time_seconds')).to.exist; 80 | expect(Prometheus.register.getSingleMetric('prefix_process_resident_memory_bytes')).to.exist; 81 | expect(Prometheus.register.getSingleMetric('prefix_nodejs_eventloop_lag_seconds')).to.exist; 82 | 83 | expect(Prometheus.register.getSingleMetric('prefix_nodejs_active_handles_total')).to.exist; 84 | expect(Prometheus.register.getSingleMetric('prefix_nodejs_active_requests_total')).to.exist; 85 | 86 | expect(Prometheus.register.getSingleMetric('prefix_nodejs_heap_size_total_bytes')).to.exist; 87 | expect(Prometheus.register.getSingleMetric('prefix_nodejs_heap_size_used_bytes')).to.exist; 88 | expect(Prometheus.register.getSingleMetric('prefix_nodejs_external_memory_bytes')).to.exist; 89 | expect(Prometheus.register.getSingleMetric('prefix_nodejs_heap_space_size_total_bytes')).to.exist; 90 | expect(Prometheus.register.getSingleMetric('prefix_nodejs_heap_space_size_used_bytes')).to.exist; 91 | expect(Prometheus.register.getSingleMetric('prefix_nodejs_heap_space_size_available_bytes')).to.exist; 92 | 93 | expect(Prometheus.register.getSingleMetric('prefix_app_version')).to.exist; 94 | }); 95 | after(() => { 96 | Prometheus.register.clear(); 97 | }); 98 | }); 99 | describe('when calling the function with options empty arrays', () => { 100 | before(async () => { 101 | await middleware({ 102 | durationBuckets: [], 103 | requestSizeBuckets: [], 104 | responseSizeBuckets: [] 105 | }); 106 | }); 107 | it('should have http_request_size_bytes metrics with default buckets', () => { 108 | expect(Object.keys(Prometheus.register.getSingleMetric('http_request_size_bytes').bucketValues)).to.have.lengthOf(0); 109 | }); 110 | it('should have http_request_size_bytes with the right labels', () => { 111 | expect(Prometheus.register.getSingleMetric('http_request_size_bytes').labelNames).to.have.members(['method', 'route', 'code']); 112 | }); 113 | it('should have http_request_duration_seconds metrics with buckets', () => { 114 | expect(Object.keys(Prometheus.register.getSingleMetric('http_request_duration_seconds').bucketValues)).to.have.lengthOf(0); 115 | }); 116 | it('should have http_request_duration_seconds with the right labels', () => { 117 | expect(Prometheus.register.getSingleMetric('http_request_duration_seconds').labelNames).to.have.members(['method', 'route', 'code']); 118 | }); 119 | it('should have http_response_size_bytes metrics with default buckets', () => { 120 | expect(Object.keys(Prometheus.register.getSingleMetric('http_response_size_bytes').bucketValues)).to.have.lengthOf(0); 121 | }); 122 | it('should have http_response_size_bytes with the right labels', () => { 123 | expect(Prometheus.register.getSingleMetric('http_response_size_bytes').labelNames).to.have.members(['method', 'route', 'code']); 124 | }); 125 | after(() => { 126 | Prometheus.register.clear(); 127 | }); 128 | }); 129 | describe('when calling the function without options', () => { 130 | before(async () => { 131 | await middleware(); 132 | }); 133 | it('should have http_request_size_bytes metrics with default buckets', () => { 134 | expect(Prometheus.register.getSingleMetric('http_request_size_bytes').bucketValues).to.have.all.keys([5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000, 10000]); 135 | }); 136 | it('should have http_request_size_bytes with the right labels', () => { 137 | expect(Prometheus.register.getSingleMetric('http_request_size_bytes').labelNames).to.have.members(['method', 'route', 'code']); 138 | }); 139 | it('should have http_request_duration_seconds metrics with default buckets', () => { 140 | expect(Prometheus.register.getSingleMetric('http_request_duration_seconds').bucketValues).to.have.all.keys([0.001, 0.005, 0.015, 0.05, 0.1, 0.2, 0.3, 0.4, 0.5]); 141 | }); 142 | it('should have http_request_duration_seconds with the right labels', () => { 143 | expect(Prometheus.register.getSingleMetric('http_request_duration_seconds').labelNames).to.have.members(['method', 'route', 'code']); 144 | }); 145 | it('should have http_response_size_bytes metrics with default buckets', () => { 146 | expect(Prometheus.register.getSingleMetric('http_response_size_bytes').bucketValues).to.have.all.keys([5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000, 10000]); 147 | }); 148 | it('should have http_response_size_bytes with the right labels', () => { 149 | expect(Prometheus.register.getSingleMetric('http_response_size_bytes').labelNames).to.have.members(['method', 'route', 'code']); 150 | }); 151 | after(() => { 152 | Prometheus.register.clear(); 153 | }); 154 | }); 155 | describe('when using the middleware request has body', () => { 156 | let func, req, res, next, requestSizeObserve, requestTimeObserve, endTimerStub; 157 | before(async () => { 158 | next = sinon.stub(); 159 | req = httpMocks.createRequest({ 160 | url: '/path', 161 | method: 'GET', 162 | body: { 163 | foo: 'bar' 164 | }, 165 | headers: { 166 | 'content-length': '25' 167 | } 168 | }); 169 | req.route = { 170 | path: '/' 171 | }; 172 | req.socket = {}; 173 | res = httpMocks.createResponse({ 174 | eventEmitter: EventEmitter 175 | }); 176 | res.statusCode = 200; 177 | func = await middleware(); 178 | endTimerStub = sinon.stub(); 179 | requestTimeObserve = sinon.stub(Prometheus.register.getSingleMetric('http_request_duration_seconds'), 'startTimer').returns(endTimerStub); 180 | func(req, res, next); 181 | }); 182 | it('should save the request size and start time on the request', () => { 183 | expect(req.metrics.contentLength).to.equal(25); 184 | }); 185 | it('should call next', () => { 186 | sinon.assert.calledOnce(next); 187 | }); 188 | describe('when the request ends', () => { 189 | before(() => { 190 | requestSizeObserve = sinon.spy(Prometheus.register.getSingleMetric('http_request_size_bytes'), 'observe'); 191 | res.emit('finish'); 192 | }); 193 | it('should update the histogram with the elapsed time and size', () => { 194 | sinon.assert.calledWithExactly(requestSizeObserve, { 195 | method: 'GET', 196 | route: '/path', 197 | code: 200 198 | }, 25); 199 | sinon.assert.called(requestTimeObserve); 200 | sinon.assert.calledWith(endTimerStub, { 201 | method: 'GET', 202 | route: '/path', 203 | code: 200 204 | }); 205 | sinon.assert.calledOnce(requestTimeObserve); 206 | sinon.assert.calledOnce(endTimerStub); 207 | }); 208 | after(() => { 209 | requestSizeObserve.restore(); 210 | requestTimeObserve.restore(); 211 | }); 212 | }); 213 | after(() => { 214 | Prometheus.register.clear(); 215 | }); 216 | }); 217 | describe('when using the middleware request has\'t body', () => { 218 | let func, req, res, next, requestTimeObserve, requestSizeObserve, endTimerStub; 219 | before(async () => { 220 | next = sinon.stub(); 221 | req = httpMocks.createRequest({ 222 | url: '/path', 223 | method: 'GET' 224 | }); 225 | req.route = { 226 | path: '/:id' 227 | }; 228 | req.socket = {}; 229 | res = httpMocks.createResponse({ 230 | eventEmitter: EventEmitter 231 | }); 232 | res.statusCode = 200; 233 | func = await middleware(); 234 | endTimerStub = sinon.stub(); 235 | requestTimeObserve = sinon.stub(Prometheus.register.getSingleMetric('http_request_duration_seconds'), 'startTimer').returns(endTimerStub); 236 | func(req, res, next); 237 | }); 238 | it('should save the request size and start time on the request', () => { 239 | expect(req.metrics.contentLength).to.equal(0); 240 | }); 241 | it('should call next', () => { 242 | sinon.assert.calledOnce(next); 243 | }); 244 | describe('when the request ends', () => { 245 | before(() => { 246 | requestSizeObserve = sinon.spy(Prometheus.register.getSingleMetric('http_request_size_bytes'), 'observe'); 247 | res.emit('finish'); 248 | }); 249 | it('should update the histogram with the elapsed time and size', () => { 250 | sinon.assert.calledWithExactly(requestSizeObserve, { 251 | method: 'GET', 252 | route: '/path/:id', 253 | code: 200 254 | }, 0); 255 | sinon.assert.called(requestTimeObserve); 256 | sinon.assert.calledWith(endTimerStub, { 257 | method: 'GET', 258 | route: '/path/:id', 259 | code: 200 260 | }); 261 | sinon.assert.calledOnce(endTimerStub); 262 | sinon.assert.calledOnce(requestTimeObserve); 263 | }); 264 | after(() => { 265 | requestSizeObserve.restore(); 266 | requestTimeObserve.restore(); 267 | }); 268 | }); 269 | after(() => { 270 | Prometheus.register.clear(); 271 | }); 272 | }); 273 | describe('when using the middleware response has body', () => { 274 | let func, req, res, next, responseSizeObserve, requestTimeObserve, endTimerStub; 275 | before(async () => { 276 | next = sinon.stub(); 277 | req = httpMocks.createRequest({ 278 | url: '/path', 279 | method: 'GET' 280 | }); 281 | req.route = { 282 | path: '/' 283 | }; 284 | req.socket = {}; 285 | res = httpMocks.createResponse({ 286 | eventEmitter: EventEmitter 287 | }); 288 | res.statusCode = 200; 289 | res._headers = { 290 | 'content-length': '25' 291 | }; 292 | func = await middleware(); 293 | responseSizeObserve = sinon.spy(Prometheus.register.getSingleMetric('http_response_size_bytes'), 'observe'); 294 | endTimerStub = sinon.stub(); 295 | requestTimeObserve = sinon.stub(Prometheus.register.getSingleMetric('http_request_duration_seconds'), 'startTimer').returns(endTimerStub); 296 | func(req, res, next); 297 | res.emit('finish'); 298 | }); 299 | it('should update the histogram with the elapsed time and size', () => { 300 | sinon.assert.calledWithExactly(responseSizeObserve, { 301 | method: 'GET', 302 | route: '/path', 303 | code: 200 304 | }, 25); 305 | sinon.assert.called(requestTimeObserve); 306 | sinon.assert.calledWith(endTimerStub, { 307 | method: 'GET', 308 | route: '/path', 309 | code: 200 310 | }); 311 | sinon.assert.calledOnce(requestTimeObserve); 312 | sinon.assert.calledOnce(endTimerStub); 313 | }); 314 | after(() => { 315 | responseSizeObserve.restore(); 316 | requestTimeObserve.restore(); 317 | Prometheus.register.clear(); 318 | }); 319 | }); 320 | describe('when using the middleware response has\'t body', () => { 321 | let func, req, res, next, responseSizeObserve, requestTimeObserve, endTimerStub; 322 | before(async () => { 323 | next = sinon.stub(); 324 | req = httpMocks.createRequest({ 325 | url: '/path', 326 | method: 'GET' 327 | }); 328 | req.route = { 329 | path: '/' 330 | }; 331 | req.socket = {}; 332 | res = httpMocks.createResponse({ 333 | eventEmitter: EventEmitter 334 | }); 335 | res.statusCode = 200; 336 | func = await middleware(); 337 | endTimerStub = sinon.stub(); 338 | responseSizeObserve = sinon.spy(Prometheus.register.getSingleMetric('http_response_size_bytes'), 'observe'); 339 | requestTimeObserve = sinon.stub(Prometheus.register.getSingleMetric('http_request_duration_seconds'), 'startTimer').returns(endTimerStub); 340 | func(req, res, next); 341 | res.emit('finish'); 342 | }); 343 | it('should update the histogram with the elapsed time and size', () => { 344 | sinon.assert.calledWithExactly(responseSizeObserve, { 345 | method: 'GET', 346 | route: '/path', 347 | code: 200 348 | }, 0); 349 | sinon.assert.called(requestTimeObserve); 350 | sinon.assert.calledWith(endTimerStub, { 351 | method: 'GET', 352 | route: '/path', 353 | code: 200 354 | }); 355 | sinon.assert.calledOnce(requestTimeObserve); 356 | sinon.assert.calledOnce(endTimerStub); 357 | }); 358 | after(() => { 359 | responseSizeObserve.restore(); 360 | requestTimeObserve.restore(); 361 | Prometheus.register.clear(); 362 | }); 363 | }); 364 | describe('override the default path', () => { 365 | let func; 366 | beforeEach(async () => { 367 | func = await middleware({ 368 | metricsPath: '/v1/metrics' 369 | }); 370 | }); 371 | it('should set the updated route', async () => { 372 | const end = sinon.stub(); 373 | const set = sinon.stub(); 374 | await func({ 375 | url: '/v1/metrics' 376 | }, { 377 | end: end, 378 | set: set 379 | }); 380 | sinon.assert.calledOnce(end); 381 | // eslint-disable-next-line no-control-regex 382 | const endFormalized = end.getCall(0).args[0].replace(/ ([0-9]*[.])?[0-9]+[\x0a]/g, ' #num\n'); 383 | // eslint-disable-next-line no-control-regex 384 | const apiFormalized = (await Prometheus.register.metrics()).replace(/ ([0-9]*[.])?[0-9]+[\x0a]/g, ' #num\n') 385 | .replace('nodejs_active_resources{type="Timeout"} #num\n', ''); // Ignoring this field because it's not working on prom-client@14+ 386 | expect(endFormalized).to.eql(apiFormalized); 387 | sinon.assert.calledWith(set, 'Content-Type', Prometheus.register.contentType); 388 | sinon.assert.calledOnce(set); 389 | }); 390 | after(() => { 391 | Prometheus.register.clear(); 392 | }); 393 | }); 394 | describe('when initialize the middleware twice', () => { 395 | let firstFunction, secondFunction; 396 | before(async () => { 397 | firstFunction = await middleware(); 398 | }); 399 | it('should throw error if metric already registered', async () => { 400 | expect((async () => { secondFunction = await middleware() })()).to.be.rejectedWith('A metric with the name process_cpu_user_seconds_total has already been registered'); 401 | }); 402 | it('should not return the same middleware function', () => { 403 | expect(firstFunction).to.not.equal(secondFunction); 404 | }); 405 | it('should have http_request_size_bytes with the right labels', () => { 406 | expect(Prometheus.register.getSingleMetric('http_request_size_bytes').labelNames).to.have.members(['method', 'route', 'code']); 407 | }); 408 | it('should have http_request_duration_seconds with the right labels', () => { 409 | expect(Prometheus.register.getSingleMetric('http_request_duration_seconds').labelNames).to.have.members(['method', 'route', 'code']); 410 | }); 411 | it('should have http_response_size_bytes with the right labels', () => { 412 | expect(Prometheus.register.getSingleMetric('http_response_size_bytes').labelNames).to.have.members(['method', 'route', 'code']); 413 | }); 414 | after(() => { 415 | Prometheus.register.clear(); 416 | }); 417 | }); 418 | describe('when using middleware request baseUrl is undefined', () => { 419 | let func, req, res, next, requestSizeObserve, requestTimeObserve, endTimerStub; 420 | before(async () => { 421 | next = sinon.stub(); 422 | req = httpMocks.createRequest({ 423 | url: '/path', 424 | method: 'GET', 425 | body: { 426 | foo: 'bar' 427 | }, 428 | headers: { 429 | 'content-length': '25' 430 | } 431 | }); 432 | req.socket = {}; 433 | req.route = { 434 | path: '/' 435 | }; 436 | res = httpMocks.createResponse({ 437 | eventEmitter: EventEmitter 438 | }); 439 | delete req.baseUrl; 440 | res.statusCode = 200; 441 | func = await middleware(); 442 | endTimerStub = sinon.stub(); 443 | requestTimeObserve = sinon.stub(Prometheus.register.getSingleMetric('http_request_duration_seconds'), 'startTimer').returns(endTimerStub); 444 | func(req, res, next); 445 | requestSizeObserve = sinon.spy(Prometheus.register.getSingleMetric('http_request_size_bytes'), 'observe'); 446 | res.emit('finish'); 447 | }); 448 | it('should update the histogram with the elapsed time and size', () => { 449 | sinon.assert.calledWithExactly(requestSizeObserve, { 450 | method: 'GET', 451 | route: '/path', 452 | code: 200 453 | }, 25); 454 | sinon.assert.called(requestTimeObserve); 455 | sinon.assert.calledWith(endTimerStub, { 456 | method: 'GET', 457 | route: '/path', 458 | code: 200 459 | }); 460 | sinon.assert.calledOnce(requestTimeObserve); 461 | sinon.assert.calledOnce(endTimerStub); 462 | }); 463 | after(() => { 464 | requestSizeObserve.restore(); 465 | requestTimeObserve.restore(); 466 | Prometheus.register.clear(); 467 | }); 468 | }); 469 | describe('when using middleware request baseUrl is undefined and path is not "/"', () => { 470 | let func, req, res, next, requestSizeObserve, requestTimeObserve, endTimerStub; 471 | before(async () => { 472 | next = sinon.stub(); 473 | req = httpMocks.createRequest({ 474 | url: '/path/:id', 475 | method: 'GET', 476 | body: { 477 | foo: 'bar' 478 | }, 479 | headers: { 480 | 'content-length': '25' 481 | } 482 | }); 483 | req.socket = {}; 484 | req.route = { 485 | path: '/:id' 486 | }; 487 | res = httpMocks.createResponse({ 488 | eventEmitter: EventEmitter 489 | }); 490 | delete req.baseUrl; 491 | res.statusCode = 200; 492 | func = await middleware(); 493 | endTimerStub = sinon.stub(); 494 | requestTimeObserve = sinon.stub(Prometheus.register.getSingleMetric('http_request_duration_seconds'), 'startTimer').returns(endTimerStub); 495 | func(req, res, next); 496 | requestSizeObserve = sinon.spy(Prometheus.register.getSingleMetric('http_request_size_bytes'), 'observe'); 497 | res.emit('finish'); 498 | }); 499 | it('should update the histogram with the elapsed time and size', () => { 500 | sinon.assert.calledWithExactly(requestSizeObserve, { 501 | method: 'GET', 502 | route: '/path/:id', 503 | code: 200 504 | }, 25); 505 | sinon.assert.called(requestTimeObserve); 506 | sinon.assert.calledWith(endTimerStub, { 507 | method: 'GET', 508 | route: '/path/:id', 509 | code: 200 510 | }); 511 | sinon.assert.calledOnce(requestTimeObserve); 512 | sinon.assert.calledOnce(endTimerStub); 513 | }); 514 | after(() => { 515 | requestSizeObserve.restore(); 516 | requestTimeObserve.restore(); 517 | Prometheus.register.clear(); 518 | }); 519 | }); 520 | describe('when _getConnections called', () => { 521 | let Middleware, server, numberOfConnectionsGauge, expressMiddleware, prometheusStub; 522 | before(() => { 523 | Middleware = require('../../src/express-middleware'); 524 | server = { 525 | getConnections: sinon.stub() 526 | }; 527 | }); 528 | describe('when there is no server', () => { 529 | before(() => { 530 | const expressMiddleware = new Middleware({}); 531 | expressMiddleware._getConnections(); 532 | }); 533 | it('should not call getConnections', () => { 534 | sinon.assert.notCalled(server.getConnections); 535 | }); 536 | }); 537 | describe('when there is server', () => { 538 | after(() => { 539 | prometheusStub.restore(); 540 | }); 541 | afterEach(() => { 542 | numberOfConnectionsGauge.set.resetHistory(); 543 | }); 544 | before(() => { 545 | numberOfConnectionsGauge = { 546 | set: sinon.stub() 547 | }; 548 | prometheusStub = sinon.stub(Prometheus.register, 'getSingleMetric').returns(numberOfConnectionsGauge); 549 | }); 550 | describe('when getConnections return count', () => { 551 | before(() => { 552 | server.getConnections = sinon.stub().yields(null, 1); 553 | expressMiddleware = new Middleware({ server: server, numberOfConnectionsGauge: numberOfConnectionsGauge }); 554 | expressMiddleware._collectDefaultServerMetrics(1000); 555 | }); 556 | it('should call numberOfConnectionsGauge.set with count', (done) => { 557 | setTimeout(() => { 558 | sinon.assert.calledOnce(server.getConnections); 559 | sinon.assert.calledOnce(numberOfConnectionsGauge.set); 560 | sinon.assert.calledWith(numberOfConnectionsGauge.set, 1); 561 | done(); 562 | }, 1100); 563 | }); 564 | }); 565 | describe('when getConnections return count', () => { 566 | before(() => { 567 | server.getConnections = sinon.stub().yields(new Error('error')); 568 | expressMiddleware = new Middleware({ server: server, numberOfConnectionsGauge: numberOfConnectionsGauge }); 569 | expressMiddleware._collectDefaultServerMetrics(500); 570 | }); 571 | it('should not call numberOfConnectionsGauge.set with count', (done) => { 572 | setTimeout(() => { 573 | sinon.assert.calledOnce(server.getConnections); 574 | sinon.assert.notCalled(numberOfConnectionsGauge.set); 575 | done(); 576 | }, 510); 577 | }); 578 | }); 579 | }); 580 | }); 581 | describe('when calling the function with additionalLabels option', () => { 582 | before(() => { 583 | middleware({ 584 | additionalLabels: ['label1', 'label2'] 585 | }); 586 | }); 587 | it('should have http_request_duration_seconds with the right labels', () => { 588 | expect(Prometheus.register.getSingleMetric('http_request_duration_seconds').labelNames).to.have.members(['method', 'route', 'code', 'label1', 'label2']); 589 | }); 590 | it('should have http_request_size_bytes with the right labels', () => { 591 | expect(Prometheus.register.getSingleMetric('http_request_size_bytes').labelNames).to.have.members(['method', 'route', 'code', 'label1', 'label2']); 592 | }); 593 | it('should have http_response_size_bytes with the right labels', () => { 594 | expect(Prometheus.register.getSingleMetric('http_response_size_bytes').labelNames).to.have.members(['method', 'route', 'code', 'label1', 'label2']); 595 | }); 596 | after(() => { 597 | Prometheus.register.clear(); 598 | }); 599 | }); 600 | describe('when using the middleware with additionalLabels options', () => { 601 | let func, req, res, next, requestSizeObserve, requestTimeObserve, endTimerStub; 602 | before(() => { 603 | next = sinon.stub(); 604 | req = httpMocks.createRequest({ 605 | url: '/path/:id', 606 | method: 'GET', 607 | body: { 608 | foo: 'bar' 609 | }, 610 | headers: { 611 | 'content-length': '25' 612 | } 613 | }); 614 | req.socket = {}; 615 | req.route = { 616 | path: '/:id' 617 | }; 618 | res = httpMocks.createResponse({ 619 | eventEmitter: EventEmitter 620 | }); 621 | delete req.baseUrl; 622 | res.statusCode = 200; 623 | func = middleware({ 624 | additionalLabels: ['label1', 'label2'], 625 | extractAdditionalLabelValuesFn: () => ({ label1: 'valueLabel1', label2: 'valueLabel2' }) 626 | }); 627 | endTimerStub = sinon.stub(); 628 | requestTimeObserve = sinon.stub(Prometheus.register.getSingleMetric('http_request_duration_seconds'), 'startTimer').returns(endTimerStub); 629 | func(req, res, next); 630 | requestSizeObserve = sinon.spy(Prometheus.register.getSingleMetric('http_request_size_bytes'), 'observe'); 631 | res.emit('finish'); 632 | }); 633 | it('metrics should include additional metrics', () => { 634 | sinon.assert.calledWithExactly(requestSizeObserve, { 635 | label1: 'valueLabel1', 636 | label2: 'valueLabel2', 637 | method: 'GET', 638 | route: '/path/:id', 639 | code: 200 640 | }, 25); 641 | sinon.assert.called(requestTimeObserve); 642 | sinon.assert.calledWith(endTimerStub, { 643 | label1: 'valueLabel1', 644 | label2: 'valueLabel2', 645 | method: 'GET', 646 | route: '/path/:id', 647 | code: 200 648 | }); 649 | sinon.assert.calledOnce(requestTimeObserve); 650 | sinon.assert.calledOnce(endTimerStub); 651 | }); 652 | after(() => { 653 | requestSizeObserve.restore(); 654 | requestTimeObserve.restore(); 655 | Prometheus.register.clear(); 656 | }); 657 | }); 658 | }); 659 | -------------------------------------------------------------------------------- /test/unit-test/utils-test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const expect = require('chai').expect; 4 | const utils = require('../../src/utils'); 5 | 6 | describe('utils', () => { 7 | describe('getMetricNames', () => { 8 | it('should include project name', () => { 9 | const metricNames = ['metric1']; 10 | const useUniqueHistogramName = true; 11 | const metricsPrefix = true; 12 | const projectName = 'mock_project'; 13 | expect(utils.getMetricNames(metricNames, useUniqueHistogramName, metricsPrefix, projectName)[0]).to.equal('mock_project_metric1'); 14 | }); 15 | }); 16 | describe('isArray', () => { 17 | it('should return true when it\'s an array', () => { 18 | expect(utils.isArray([])).to.equal(true); 19 | }); 20 | it('should return false when it\'s not an array', () => { 21 | expect(utils.isArray(null)).to.equal(false); 22 | expect(utils.isArray(undefined)).to.equal(false); 23 | expect(utils.isArray('string')).to.equal(false); 24 | expect(utils.isArray(true)).to.equal(false); 25 | }); 26 | }); 27 | describe('isFunction', () => { 28 | it('should return true when it\'s a function', () => { 29 | expect(utils.isFunction(() => {})).to.equal(true); 30 | }); 31 | it('should return false when it\'s not a function', () => { 32 | expect(utils.isFunction(null)).to.equal(false); 33 | expect(utils.isFunction(undefined)).to.equal(false); 34 | expect(utils.isFunction('string')).to.equal(false); 35 | expect(utils.isFunction(true)).to.equal(false); 36 | expect(utils.isFunction([])).to.equal(false); 37 | }); 38 | }); 39 | describe('isString', () => { 40 | it('should return true when it\'s a string', () => { 41 | expect(utils.isString('string')).to.equal(true); 42 | }); 43 | it('should return false when it\'s not a string', () => { 44 | expect(utils.isString(null)).to.equal(false); 45 | expect(utils.isString(undefined)).to.equal(false); 46 | expect(utils.isString(true)).to.equal(false); 47 | expect(utils.isString([])).to.equal(false); 48 | expect(utils.isString(() => {})).to.equal(false); 49 | }); 50 | }); 51 | describe('shouldLogMetrics', () => { 52 | it('should return true if route is not excluded', () => { 53 | const excludeRoutes = ['route1', 'route2']; 54 | const route = 'route'; 55 | expect(utils.shouldLogMetrics(excludeRoutes, route)).to.equal(true); 56 | }); 57 | it('should return false if route is excluded', () => { 58 | const excludeRoutes = ['route1', 'route2']; 59 | const route = 'route1'; 60 | expect(utils.shouldLogMetrics(excludeRoutes, route)).to.equal(false); 61 | }); 62 | }); 63 | describe('validateInput', () => { 64 | it('should return input if valid', () => { 65 | const value = utils.validateInput({ 66 | input: 'string', 67 | isValidInputFn: utils.isString, 68 | defaultValue: 'default-string' 69 | }); 70 | expect(value).to.equal('string'); 71 | }); 72 | it('should return input value if empty string', () => { 73 | const value = utils.validateInput({ 74 | input: '', 75 | isValidInputFn: utils.isString, 76 | defaultValue: 'default-string' 77 | }); 78 | expect(value).to.equal(''); 79 | }); 80 | it('should return input value if zero', () => { 81 | const value = utils.validateInput({ 82 | input: 0, 83 | isValidInputFn: (input) => typeof input === 'number', 84 | defaultValue: 100 85 | }); 86 | expect(value).to.equal(0); 87 | }); 88 | it('should return default value if input is undefined', () => { 89 | const value = utils.validateInput({ 90 | isValidInputFn: utils.isString, 91 | defaultValue: 'default-string' 92 | }); 93 | expect(value).to.equal('default-string'); 94 | }); 95 | it('should throw if input is not valid', () => { 96 | const fn = utils.validateInput.bind(utils.validateInput, { 97 | input: true, 98 | isValidInputFn: utils.isString, 99 | defaultValue: 'default-string', 100 | errorMessage: 'error message' 101 | }); 102 | expect(fn).to.throw('error message'); 103 | }); 104 | }); 105 | }); 106 | -------------------------------------------------------------------------------- /test/utils/sleep.js: -------------------------------------------------------------------------------- 1 | module.exports = require('util').promisify(setTimeout); -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": false, 5 | "noImplicitAny": false, 6 | "removeComments": true, 7 | "noLib": false, 8 | "allowSyntheticDefaultImports": true, 9 | "emitDecoratorMetadata": true, 10 | "experimentalDecorators": true, 11 | "target": "es6", 12 | "sourceMap": true, 13 | "allowJs": true, 14 | "outDir": "./dist", 15 | "baseUrl": "./src", 16 | "lib": [ "es2017" ] 17 | }, 18 | "include": [ 19 | "src/**/*" 20 | ], 21 | "exclude": [ 22 | "node_modules", 23 | "**/*.spec.ts" 24 | ] 25 | } --------------------------------------------------------------------------------