Compare commits

...

71 Commits
6.3.2 ... 7.0.2

Author SHA1 Message Date
Konstantin Pogorelov
fd33d98c15 bump v7.0.2 2024-10-12 13:54:58 +02:00
Konstantin Pogorelov
5978ea7b73 Merge remote-tracking branch 'remotes/origin/dependabot/npm_and_yarn/multi-092c445592' 2024-10-12 13:51:57 +02:00
Konstantin Pogorelov
71467e6a17 Merge remote-tracking branch 'remotes/origin/dependabot/npm_and_yarn/multi-6b7e5c81f3' 2024-10-12 13:51:45 +02:00
Konstantin Pogorelov
c6b24f6eca Merge remote-tracking branch 'remotes/origin/dependabot/npm_and_yarn/multi-9f37c16f8f' 2024-10-12 13:51:31 +02:00
Konstantin Pogorelov
1ac5fba5c4 Merge remote-tracking branch 'remotes/origin/dependabot/npm_and_yarn/multi-41ee8087b2' 2024-10-12 13:51:11 +02:00
dependabot[bot]
ca8b0ba1e0 Bump body-parser and express
Bumps [body-parser](https://github.com/expressjs/body-parser) to 1.20.3 and updates ancestor dependency [express](https://github.com/expressjs/express). These dependencies need to be updated together.


Updates `body-parser` from 1.20.2 to 1.20.3
- [Release notes](https://github.com/expressjs/body-parser/releases)
- [Changelog](https://github.com/expressjs/body-parser/blob/master/HISTORY.md)
- [Commits](https://github.com/expressjs/body-parser/compare/1.20.2...1.20.3)

Updates `express` from 4.19.2 to 4.21.1
- [Release notes](https://github.com/expressjs/express/releases)
- [Changelog](https://github.com/expressjs/express/blob/4.21.1/History.md)
- [Commits](https://github.com/expressjs/express/compare/4.19.2...4.21.1)

---
updated-dependencies:
- dependency-name: body-parser
  dependency-type: indirect
- dependency-name: express
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-12 11:48:55 +00:00
dependabot[bot]
2ebc2618de Bump path-to-regexp and express
Bumps [path-to-regexp](https://github.com/pillarjs/path-to-regexp) to 0.1.10 and updates ancestor dependency [express](https://github.com/expressjs/express). These dependencies need to be updated together.


Updates `path-to-regexp` from 0.1.7 to 0.1.10
- [Release notes](https://github.com/pillarjs/path-to-regexp/releases)
- [Changelog](https://github.com/pillarjs/path-to-regexp/blob/master/History.md)
- [Commits](https://github.com/pillarjs/path-to-regexp/compare/v0.1.7...v0.1.10)

Updates `express` from 4.19.2 to 4.21.1
- [Release notes](https://github.com/expressjs/express/releases)
- [Changelog](https://github.com/expressjs/express/blob/4.21.1/History.md)
- [Commits](https://github.com/expressjs/express/compare/4.19.2...4.21.1)

---
updated-dependencies:
- dependency-name: path-to-regexp
  dependency-type: indirect
- dependency-name: express
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-12 11:48:51 +00:00
dependabot[bot]
df46ecaa9a Bump cookie and express
Bumps [cookie](https://github.com/jshttp/cookie) to 0.7.1 and updates ancestor dependency [express](https://github.com/expressjs/express). These dependencies need to be updated together.


Updates `cookie` from 0.6.0 to 0.7.1
- [Release notes](https://github.com/jshttp/cookie/releases)
- [Commits](https://github.com/jshttp/cookie/compare/v0.6.0...v0.7.1)

Updates `express` from 4.19.2 to 4.21.1
- [Release notes](https://github.com/expressjs/express/releases)
- [Changelog](https://github.com/expressjs/express/blob/4.21.1/History.md)
- [Commits](https://github.com/expressjs/express/compare/4.19.2...4.21.1)

---
updated-dependencies:
- dependency-name: cookie
  dependency-type: indirect
- dependency-name: express
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-12 11:48:01 +00:00
dependabot[bot]
abdfe2d93a Bump serve-static and express
Bumps [serve-static](https://github.com/expressjs/serve-static) to 1.16.2 and updates ancestor dependency [express](https://github.com/expressjs/express). These dependencies need to be updated together.


Updates `serve-static` from 1.15.0 to 1.16.2
- [Release notes](https://github.com/expressjs/serve-static/releases)
- [Changelog](https://github.com/expressjs/serve-static/blob/v1.16.2/HISTORY.md)
- [Commits](https://github.com/expressjs/serve-static/compare/v1.15.0...v1.16.2)

Updates `express` from 4.19.2 to 4.21.1
- [Release notes](https://github.com/expressjs/express/releases)
- [Changelog](https://github.com/expressjs/express/blob/4.21.1/History.md)
- [Commits](https://github.com/expressjs/express/compare/4.19.2...4.21.1)

---
updated-dependencies:
- dependency-name: serve-static
  dependency-type: indirect
- dependency-name: express
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-12 11:48:00 +00:00
dependabot[bot]
33ab388106 Bump send and express
Bumps [send](https://github.com/pillarjs/send) to 0.19.0 and updates ancestor dependency [express](https://github.com/expressjs/express). These dependencies need to be updated together.


Updates `send` from 0.18.0 to 0.19.0
- [Release notes](https://github.com/pillarjs/send/releases)
- [Changelog](https://github.com/pillarjs/send/blob/master/HISTORY.md)
- [Commits](https://github.com/pillarjs/send/compare/0.18.0...0.19.0)

Updates `express` from 4.19.2 to 4.21.1
- [Release notes](https://github.com/expressjs/express/releases)
- [Changelog](https://github.com/expressjs/express/blob/4.21.1/History.md)
- [Commits](https://github.com/expressjs/express/compare/4.19.2...4.21.1)

---
updated-dependencies:
- dependency-name: send
  dependency-type: indirect
- dependency-name: express
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-12 11:47:58 +00:00
Konstantin Pogorelov
02865e531d bump v7.0.1 2024-10-12 13:44:03 +02:00
Konstantin Pogorelov
1a1d8e0b54 Merge pull request #126 from jochen-schweizer/dependabot/npm_and_yarn/express-4.19.2
Bump express from 4.18.2 to 4.19.2
2024-10-12 13:38:00 +02:00
Konstantin Pogorelov
8371c551d5 Merge pull request #127 from jochen-schweizer/dependabot/npm_and_yarn/braces-3.0.3
Bump braces from 3.0.2 to 3.0.3
2024-10-12 13:37:44 +02:00
dependabot[bot]
10a58635e1 Bump braces from 3.0.2 to 3.0.3
Bumps [braces](https://github.com/micromatch/braces) from 3.0.2 to 3.0.3.
- [Changelog](https://github.com/micromatch/braces/blob/master/CHANGELOG.md)
- [Commits](https://github.com/micromatch/braces/compare/3.0.2...3.0.3)

---
updated-dependencies:
- dependency-name: braces
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-21 06:23:20 +00:00
dependabot[bot]
5f44228f69 Bump express from 4.18.2 to 4.19.2
Bumps [express](https://github.com/expressjs/express) from 4.18.2 to 4.19.2.
- [Release notes](https://github.com/expressjs/express/releases)
- [Changelog](https://github.com/expressjs/express/blob/master/History.md)
- [Commits](https://github.com/expressjs/express/compare/4.18.2...4.19.2)

---
updated-dependencies:
- dependency-name: express
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-29 15:28:24 +00:00
Timm Stelzer
fd5ff1cfe0 feat: respect pruneAgedBuckets, update README 2024-01-06 11:31:26 +01:00
Konstantin Pogorelov
edfb9992ed update README, add .env to gitignore 2024-01-06 11:17:51 +01:00
Konstantin Pogorelov
0ce44722a5 use collectDefaultMetrics.prefix for up metric 2024-01-06 10:48:57 +01:00
Konstantin Pogorelov
407ea4b0d7 fix unittest for prom-client 15 2024-01-06 10:26:33 +01:00
Steve
63b6d89caa Update prom-client to v15 2024-01-05 23:58:13 +01:00
Konstantin Pogorelov
5e2b284903 Merge pull request #102 from Cellaryllis/normalize-path-example-3
Add another example for normalizePath
2024-01-05 23:46:43 +01:00
Konstantin Pogorelov
c693affcac Merge pull request #120 from petuomin/statuscode-for-request-closed-prematurely
Use statuscode 499 for requests that are closed before response is sent
2024-01-05 23:45:10 +01:00
Konstantin Pogorelov
a0eef5d0e2 switch from dtslint to tsd, update required node to >=18, bump version to 7.0.0 2024-01-05 22:04:31 +01:00
Pasi Tuominen
efbab7dcdb Use statuscode 499 for requests that are closed before response is sent
This happens, for example, when a http proxy in front of the application is configured with a timeout and the node server is too slow to respond.

Currently such timeouts are counted as 200s by express-prom-bundle. This PR changes that to 499 "Client Closed Request". This way it's possible to tell them apart.
2023-05-05 14:49:02 +03:00
Konstantin Pogorelov
0bda9934c1 bump 6.6.0 2022-12-15 09:18:12 +01:00
Konstantin Pogorelov
5bf6b153e6 Merge pull request #110 from ineentho/bypass-on-finish
Add new option bypassOnFinish, for when response is required in bypass callback
2022-12-15 09:14:12 +01:00
Henrik Karlsson
1171fb5be1 Combined bypass and bypassOnFinish into one option 2022-12-14 17:02:54 +01:00
Henrik Karlsson
ea9f34aa3e Add new option bypassOnFinish, for when response is required in bypass callback 2022-12-09 17:45:27 +01:00
Cellaryllis
c541824657 Move details below example 2 2022-07-12 10:18:11 +01:00
Cellaryllis
7eae5d4b7f Add another example for normalizePath 2022-07-12 10:15:47 +01:00
Konstantin Pogorelov
93b0e8fafe bump 6.5.0 2022-06-18 12:24:50 +02:00
Konstantin Pogorelov
2d2b253567 minor codestyle fixes, turn async/await in metricsApp into a conventional promise 2022-06-18 12:23:59 +02:00
Konstantin Pogorelov
1b52c6f46e Merge pull request #101 from derangeddk/add-metricsapp-option
Add metricsapp option
2022-06-18 12:08:49 +02:00
Asbjørn Dyhrberg Thegler
2293d1cd40 Remove exclusive test running
Silly me
2022-06-17 21:19:53 +02:00
Asbjørn Dyhrberg Thegler
1ee094eb04 Add jasmine test 2022-06-17 21:01:14 +02:00
Asbjørn Dyhrberg Thegler
a1284ec3e5 Add test for typings 2022-06-17 21:01:03 +02:00
Asbjørn Dyhrberg Thegler
5a1efd7fe6 Add typings 2022-06-17 20:42:35 +02:00
Asbjørn Dyhrberg Thegler
bcac9f523a Add option to README 2022-06-17 13:42:54 +02:00
Asbjørn Dyhrberg Thegler
c4be86b651 Add metricsApp option
Lets you supply an additional express app on which to mount the metrics 
endpoint
2022-06-17 13:41:36 +02:00
Konstantin Pogorelov
5e5a47a500 Merge pull request #97 from diegoximenes/fix/cluster_example
Fix cluster example
2021-10-26 09:23:45 +02:00
Diego Ximenes
426e4d4556 Fix cluster example 2021-10-26 01:47:19 -03:00
Konstantin Pogorelov
78ebb06a6d bump 6.4.1 2021-08-17 10:49:06 +03:00
Konstantin Pogorelov
c6d2210062 add urlPathReplacement to types, see #75 2021-08-17 10:37:12 +03:00
Konstantin Pogorelov
c436fa3c88 bump 6.4.0 2021-08-17 10:11:59 +03:00
Konstantin Pogorelov
77b02c7302 add README docs for urlPathReplacement, see #75 2021-08-17 10:11:10 +03:00
Konstantin Pogorelov
068ed93a61 Merge pull request #87 from prabhumarappan/add-url-value-replacement
Added replacement option for path
2021-08-17 10:03:20 +03:00
Konstantin Pogorelov
c5aa3f7388 Merge pull request #89 from jochen-schweizer/dependabot/npm_and_yarn/path-parse-1.0.7
Bump path-parse from 1.0.6 to 1.0.7
2021-08-17 10:01:56 +03:00
Konstantin Pogorelov
ca28ff4fe1 Merge pull request #85 from jochen-schweizer/dependabot/npm_and_yarn/lodash-4.17.21
Bump lodash from 4.17.19 to 4.17.21
2021-08-17 10:01:10 +03:00
Konstantin Pogorelov
52f47ed9f4 Merge pull request #84 from jochen-schweizer/dependabot/npm_and_yarn/handlebars-4.7.7
Bump handlebars from 4.7.6 to 4.7.7
2021-08-17 10:00:53 +03:00
Konstantin Pogorelov
a60996ebc3 Merge pull request #93 from RyanBard/update_types_with_exclude_routes
closes #92: Add excludeRoutes to types
2021-08-16 08:58:00 +03:00
Ryan Bard
e7c94ff307 closes #92: Add excludeRoutes to types
Signed-off-by: Ryan Bard <john.ryan.bard@gmail.com>
2021-08-15 20:04:26 -04:00
dependabot[bot]
884617bffe Bump path-parse from 1.0.6 to 1.0.7
Bumps [path-parse](https://github.com/jbgutierrez/path-parse) from 1.0.6 to 1.0.7.
- [Release notes](https://github.com/jbgutierrez/path-parse/releases)
- [Commits](https://github.com/jbgutierrez/path-parse/commits/v1.0.7)

---
updated-dependencies:
- dependency-name: path-parse
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-08-10 17:34:10 +00:00
Prabhu Marappan
32b76d0970 Added replacement option for path 2021-06-06 16:51:53 +05:30
dependabot[bot]
845cd6ee46 Bump lodash from 4.17.19 to 4.17.21
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.19 to 4.17.21.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.19...4.17.21)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-06 21:16:33 +00:00
dependabot[bot]
0c3c949425 Bump handlebars from 4.7.6 to 4.7.7
Bumps [handlebars](https://github.com/wycats/handlebars.js) from 4.7.6 to 4.7.7.
- [Release notes](https://github.com/wycats/handlebars.js/releases)
- [Changelog](https://github.com/handlebars-lang/handlebars.js/blob/master/release-notes.md)
- [Commits](https://github.com/wycats/handlebars.js/compare/v4.7.6...v4.7.7)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-06 17:31:28 +00:00
Konstantin Pogorelov
1ec800cde7 bump 6.3.6 2021-04-03 10:46:38 +02:00
Konstantin Pogorelov
661e7d5092 add types for metricType: summary 2021-04-03 10:39:46 +02:00
Konstantin Pogorelov
b569a71130 Merge pull request #83 from jochen-schweizer/dependabot/npm_and_yarn/y18n-4.0.1
Bump y18n from 4.0.0 to 4.0.1
2021-04-03 10:50:23 +02:00
dependabot[bot]
10c372c438 Bump y18n from 4.0.0 to 4.0.1
Bumps [y18n](https://github.com/yargs/y18n) from 4.0.0 to 4.0.1.
- [Release notes](https://github.com/yargs/y18n/releases)
- [Changelog](https://github.com/yargs/y18n/blob/master/CHANGELOG.md)
- [Commits](https://github.com/yargs/y18n/commits)

Signed-off-by: dependabot[bot] <support@github.com>
2021-03-30 15:50:49 +00:00
Konstantin Pogorelov
3599c32876 bump 6.3.5 2021-03-28 21:59:44 +02:00
Konstantin Pogorelov
59ec729cb2 Merge pull request #81 from matheuslc/patch-1
Describes httpDurationMetricName option on README
2021-03-28 22:09:32 +02:00
Matheus Lucca do Carmo
c95cd1834c doc: Describes httpDurationMetricName option
Changing the name of the metric, you can use an already existent name and see your application in the same metric space.
2021-03-16 17:48:28 -03:00
Konstantin Pogorelov
d18316fee8 bump 6.3.4 2021-02-22 15:21:48 +01:00
Konstantin Pogorelov
ccb74f99b0 add an a sample with actual RegExp in advanced-example.js, relates to #80 2021-02-22 15:15:37 +01:00
Konstantin Pogorelov
2e093bc14d fix codestyle in types to keep dtslint happy, relates to #80 2021-02-22 15:15:00 +01:00
Konstantin Pogorelov
3f6b2746b9 Merge pull request #80 from thewizarodofoz/patch-1
urlValueParser extra and replace masks can be regexp
2021-02-22 14:59:11 +01:00
Oz Weiss
83ee0ce06e urlValueParser extra and replace masks can be regexp
https://github.com/disjunction/url-value-parser/blob/master/src/ValueDetector.js#L39

`replaceMasks` and `extraMasks` which are passed down to `url-value-parser` can be either string or RegExp.
2021-02-22 10:48:58 +02:00
Konstantin Pogorelov
5300d0ef82 bump 6.3.3 2021-02-19 12:46:56 +01:00
Konstantin Pogorelov
62abb62772 suppress error logging by providing NODE_ENV=test, relates to #78 2021-02-19 12:42:27 +01:00
Konstantin Pogorelov
bef92b77e1 Merge pull request #78 from FauxFaux/fix/handle-errors
fix: handle errors from prom-client
2021-02-19 12:17:05 +01:00
Chris West (Faux)
99d8fc1ea9 fix: handle errors from prom-client 2021-02-19 08:32:48 +00:00
17 changed files with 3510 additions and 1845 deletions

2
.gitignore vendored
View File

@@ -2,4 +2,4 @@
node_modules
coverage
/.vscode
.env

View File

@@ -1,8 +1,6 @@
language: node_js
node_js:
- "10"
- "12"
- "14"
- "18"
notifications:
email: false
before_install:
@@ -10,4 +8,4 @@ before_install:
script:
- npm run lint
- npm test
- npm run dtslint-next
- npm run test-types

View File

@@ -1,10 +1,10 @@
.PHONY: coverage
test:
./node_modules/jasme/run.js
npm test
lint:
node_modules/eslint/bin/eslint.js src
node_modules/.bin/dtslint types
npx eslint src
npm run dtslint-next
coverage:
node_modules/istanbul/lib/cli.js cover \
-i 'src/*' \

View File

@@ -4,7 +4,9 @@
Express middleware with popular prometheus metrics in one bundle. It's also compatible with koa v1 and v2 (see below).
Since version 5 it uses **prom-client** as a peer dependency. See: https://github.com/siimon/prom-client
This library uses **prom-client v15+** as a peer dependency. See: https://github.com/siimon/prom-client
If you need a support for older versions of prom-client (v12-v14), downgrade to express-prom-bundle v6.6.0
Included metrics:
@@ -52,7 +54,7 @@ Which labels to include in `http_request_duration_seconds` metric:
* **includeUp**: include an auxiliary "up"-metric which always returns 1, default: **true**
* **metricsPath**: replace the `/metrics` route with a **regex** or exact **string**. Note: it is highly recommended to just stick to the default
* **metricType**: histogram/summary selection. See more details below
* **bypass**: function taking express request as an argument and determines whether the given request should be excluded in the metrics, default: **() => false**
* **httpDurationMetricName**: Allows you change the name of HTTP duration metric, default: **`http_request_duration_seconds`**.
### metricType option ###
@@ -67,6 +69,7 @@ Additional options for **summary**:
* **percentiles**: percentiles used for `http_request_duration_seconds` summary
* **ageBuckets**: ageBuckets configures how many buckets we have in our sliding window for the summary
* **maxAgeSeconds**: the maxAgeSeconds will tell how old a bucket can be before it is reset
* **pruneAgedBuckets**: When enabled, timed out buckets will be removed entirely. By default, buckets are reset to 0.
### Transformation callbacks ###
@@ -79,14 +82,29 @@ Additional options for **summary**:
See the [docs](https://github.com/disjunction/url-value-parser) of url-value-parser module for details.
* **formatStatusCode**: `function(res)` producing final status code from express `res` object, e.g. you can combine `200`, `201` and `204` to just `2xx`.
* **transformLabels**: `function(labels, req, res)` transforms the **labels** object, e.g. setting dynamic values to **customLabels**
* **urlPathReplacement**: replacement string for the values (default: "#val")
### Other options ###
* **autoregister**: if `/metrics` endpoint should be registered. (Default: **true**)
* **autoregister**: if `/metrics` endpoint should be registered (default: **true**)
* **promClient**: options for promClient startup, e.g. **collectDefaultMetrics**. This option was added
to keep `express-prom-bundle` runnable using confit (e.g. with kraken.js) without writing any JS code,
see [advanced example](https://github.com/jochen-schweizer/express-prom-bundle/blob/master/advanced-example.js)
* **promRegistry**: Optional `promClient.Registry` instance to attach metrics to. Defaults to global `promClient.register`.
* **metricsApp**: Allows you to attach the metrics endpoint to a different express app. You probably want to use it in combination with `autoregister: false`.
* **bypass**: An object that takes onRequest and onFinish callbacks that determines whether the given request should be excluded in the metrics. Default:
```js
{
onRequest: (req) => false,
onFinish: (req, res) => false
}
```
`onRequest` is run directly in the middleware chain, before the request is processed. `onFinish` is run after the request has been processed, and has access to the express response object in addition to the request object. Both callbacks are optional, and if one or both returns true the request is excluded.
As a shorthand, just the onRequest callback can be used instead of the object.
### More details on includePath option
@@ -144,6 +162,17 @@ For more details:
* [normalizePath.js](https://github.com/jochen-schweizer/express-prom-bundle/blob/master/src/normalizePath.js) - source code for path processing
#### Example 3 (return express route definition):
```javascript
app.use(promBundle(/* options? */));
promBundle.normalizePath = (req, opts) => {
// Return the path of the express route (i.e. /v1/user/:id or /v1/timer/automated/:userid/:timerid")
return req.route?.path ?? "NULL";
};
```
## express example
setup std. metrics but exclude `up`-metric:
@@ -197,7 +226,8 @@ which returns an aggregate of all metrics from all the workers.
``` javascript
const cluster = require('cluster');
const promBundle = require('./src/index');
const promBundle = require('express-prom-bundle');
const promClient = require('prom-client');
const numCPUs = Math.max(2, require('os').cpus().length);
const express = require('express');
@@ -213,6 +243,7 @@ if (cluster.isMaster) {
console.log('cluster metrics listening on 9999');
console.log('call localhost:9999/metrics for aggregated metrics');
} else {
new promClient.AggregatorRegistry();
const app = express();
app.use(promBundle({
autoregister: false, // disable /metrics for single workers

View File

@@ -3,6 +3,8 @@
const express = require('express');
const app = express();
const promClient = require('prom-client');
// replace this with require('.') when running from library code
const promBundle = require('express-prom-bundle');
const bundle = promBundle({
@@ -19,7 +21,8 @@ const bundle = promBundle({
urlValueParser: {
minHexLength: 5,
extraMasks: [
"^[0-9]+\\.[0-9]+\\.[0-9]+$" // replace dot-separated dates with #val
"^[0-9]+\\.[0-9]+\\.[0-9]+$", // replace dot-separated dates with #val, (regex as string)
/^[0-9]+\-[0-9]+\-[0-9]+$/ // replace dash-separated dates with #val (actual regex)
]
},
normalizePath: [

4898
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "express-prom-bundle",
"version": "6.3.2",
"version": "7.0.2",
"description": "express middleware with popular prometheus metrics in one bundle",
"main": "src/index.js",
"keywords": [
@@ -14,43 +14,42 @@
"src",
"types/index.d.ts"
],
"types": "types",
"types": "types/index.d.ts",
"scripts": {
"test": "node_modules/jasme/run.js",
"test": "NODE_ENV=test node_modules/jasme/run.js",
"lint": "eslint src",
"coverage": "make coverage",
"dtslint": "dtslint types",
"dtslint-next": "dtslint --onlyTestTsNext types"
"test-types": "tsd"
},
"author": "Konstantin Pogorelov <or@pluseq.com>",
"license": "MIT",
"dependencies": {
"@types/express": "^4.17.21",
"express": "^4.18.2",
"on-finished": "^2.3.0",
"url-value-parser": "^2.0.0"
},
"devDependencies": {
"@types/express": "^4.16.1",
"coveralls": "^3.0.2",
"dtslint": "^0.7.1",
"dts": "^0.1.1",
"eslint": "^5.11.0",
"express": "^4.16.4",
"istanbul": "^0.4.5",
"jasme": "^6.0.0",
"koa": "^2.6.2",
"koa-connect": "^2.0.1",
"prom-client": "^13.0.0",
"prom-client": "^15.0.0",
"supertest": "^3.3.0",
"supertest-koa-agent": "^0.3.0",
"tsd": "^0.30.3",
"typescript": "^3.4.5"
},
"peerDependencies": {
"prom-client": ">=12.0.0"
"prom-client": ">=15.0.0"
},
"repository": {
"type": "git",
"url": "https://github.com/jochen-schweizer/express-prom-bundle.git"
},
"engines": {
"node": ">=10"
"node": ">=18"
}
}

View File

@@ -10,6 +10,20 @@ const supertestKoa = require('supertest-koa-agent');
const promClient = require('prom-client');
const cluster = require('cluster');
// for some reason in prom-client 15 the hashmap has a trailing comma
function extractBucket (instance, key) {
const hashmap = instance.metrics.http_request_duration_seconds.hashMap;
if (hashmap[key]) {
return hashmap[key];
} else {
return hashmap[key + ','];
}
}
function getMetricCount (s) {
return s.replace(/^#.*$\n|^$\n/gm, '').trim().split('\n').length;
}
describe('index', () => {
beforeEach(() => {
promClient.register.clear();
@@ -158,10 +172,9 @@ describe('index', () => {
agent
.get('/test')
.end(() => {
const metricHashMap = instance.metrics.http_request_duration_seconds.hashMap;
expect(metricHashMap['status_code:200']).toBeDefined();
const labeled = metricHashMap['status_code:200'];
expect(labeled.count).toBe(1);
const bucket = extractBucket(instance, 'status_code:200');
expect(bucket).toBeDefined();
expect(bucket.count).toBe(1);
agent
.get('/metrics')
@@ -206,7 +219,7 @@ describe('index', () => {
bypass: (req)=> {
// metrics added here to attempt skipping /metrics
// this should fail though, because serving /metrics preceeds bypassing
return !!req.url.match(/test|bad.word|metrics/)
return !!req.url.match(/test|bad.word|metrics/);
}
});
app.use(instance);
@@ -223,11 +236,11 @@ describe('index', () => {
agent
.get('/good-word')
.end(() => {
const metricHashMap = instance.metrics.http_request_duration_seconds.hashMap;
expect(metricHashMap['status_code:200']).toBeDefined();
const bucket = extractBucket(instance, 'status_code:200');
expect(bucket).toBeDefined();
// only /good-word should be counted
expect(metricHashMap['status_code:200'].count).toBe(1);
expect(bucket.count).toBe(1);
agent
.get('/metrics')
@@ -240,6 +253,44 @@ describe('index', () => {
});
});
it('bypass requests, checking res', done => {
const app = express();
const instance = bundle({
bypass: {
onFinish: (req, res) => res.statusCode === 404
}
});
app.use(instance);
app.use('/200', (req, res) => res.send(''));
app.use('/404', (req, res) => res.status(404).send(''));
app.use('/500', (req, res) => res.status(500).send(''));
const agent = supertest(app);
agent.get('/200')
.expect(200)
.then(() => {
return agent
.get('/404')
.expect(404);
})
.then(() => {
return agent
.get('/500')
.expect(500);
})
.then(() => {
// only /200 and /500 should be counted
expect(extractBucket(instance, 'status_code:200').count).toBe(1);
expect(extractBucket(instance, 'status_code:404')).not.toBeDefined();
expect(extractBucket(instance, 'status_code:500').count).toBe(1);
return agent
.get('/metrics')
.expect(200);
})
.then(done);
});
it('complains about deprecated options', () => {
expect(() => bundle({prefix: 'hello'})).toThrow();
});
@@ -295,7 +346,7 @@ describe('index', () => {
describe('usage of normalizePath()', () => {
it('normalizePath can be replaced gloablly', done => {
it('normalizePath can be replaced globally', done => {
const app = express();
const original = bundle.normalizePath;
bundle.normalizePath = () => 'dummy';
@@ -319,6 +370,40 @@ describe('index', () => {
});
});
it('respects pruneAgedBuckets', done => {
const app = express();
const metricsApp = express();
const bundled = bundle({
metricsApp,
metricType: 'summary',
includePath: true,
maxAgeSeconds: 1,
percentiles: [0.5],
ageBuckets: 1,
pruneAgedBuckets: true,
});
app.use(bundled);
const agent = supertest(app);
const metricsAgent = supertest(metricsApp);
agent.get('/foo')
.then(() => metricsAgent.get('/metrics'))
.then(response => {
expect(response.status).toBe(200);
// up + bucket, sum, count
expect(getMetricCount(response.text)).toBe(4);
})
.then(() => new Promise(r => setTimeout(r, 1010)))
.then(() => metricsAgent.get('/metrics'))
.then(response => {
expect(response.status).toBe(200);
// only up
expect(getMetricCount(response.text)).toBe(1);
})
.finally(done);
});
it('normalizePath function can be overridden', done => {
const app = express();
const instance = bundle({
@@ -409,6 +494,27 @@ describe('index', () => {
});
});
it('handles errors in collectors', done => {
const app = express();
const instance = bundle({});
app.use(instance);
new promClient.Gauge({
name: 'kaboom',
help: 'this metric explodes',
collect() {
throw new Error('kaboom!');
}
});
// the error will NOT be displayed if NODE_ENV=test (as defined in package.json)
supertest(app)
.get('/metrics')
.expect(500)
.end((err) => done(err));
});
it('customLabels={foo: "bar"} adds foo="bar" label to metrics', done => {
const app = express();
const instance = bundle({
@@ -478,31 +584,53 @@ describe('index', () => {
});
});
it('calls promClient.collectDefaultMetrics', () => {
const spy = spyOn(promClient, 'collectDefaultMetrics');
bundle({
promClient: {
collectDefaultMetrics: {
describe('option collectDefaultMetrics', () => {
it('it gets called', () => {
const spy = spyOn(promClient, 'collectDefaultMetrics');
bundle({
promClient: {
collectDefaultMetrics: {
}
}
}
});
expect(spy).toHaveBeenCalledWith({});
});
it('prefix is used for up metric', (done) => {
const instance = bundle({
promClient: {
collectDefaultMetrics: {
prefix: 'hello_'
}
}
});
const app = express();
app.use(instance);
const agent = supertest(app);
agent
.get('/metrics')
.end((err, res) => {
expect(res.status).toBe(200);
expect(res.text).toMatch(/^hello_up\s1/m);
done();
});
});
expect(spy).toHaveBeenCalledWith({});
});
describe('usage of clusterMetrics()', () => {
it('clusterMetrics returns 200 even without a cluster', (done) => {
const app = express();
const app = express();
cluster.workers = [];
cluster.workers = [];
app.use('/cluster_metrics', bundle.clusterMetrics());
const agent = supertest(app);
agent
.get('/cluster_metrics')
.end((err, res) => {
expect(res.status).toBe(200);
done();
});
app.use('/cluster_metrics', bundle.clusterMetrics());
const agent = supertest(app);
agent
.get('/cluster_metrics')
.end((err, res) => {
expect(res.status).toBe(200);
done();
});
});
it('clusterMetrics returns 500 in case of an error', (done) => {
@@ -569,5 +697,23 @@ describe('index', () => {
});
});
});
it('additional metricsApp can be used', done => {
const app = express();
const metricsApp = express();
const bundled = bundle({metricsApp});
app.use(bundled);
const agent = supertest(app);
const metricsAgent = supertest(metricsApp);
agent.get('/').end(() => {
metricsAgent.get('/metrics').end((err, res) => {
expect(res.status).toBe(200);
expect(res.text).toMatch(/status_code="404"/);
done();
});
});
});
});
});

View File

@@ -28,4 +28,9 @@ describe('normalizePath', () => {
});
expect(subject).toThrow();
});
it('uses urlPathReplacement when passed to transform the path', () => {
expect(normalizePath({url: '/a/12345'}, {urlPathReplacement: ':id'}))
.toBe('/a/:id');
});
});

View File

@@ -6,7 +6,11 @@ const normalizeStatusCode = require('../src/normalizeStatusCode');
describe('normalizeStatusCode', () => {
it('returns run callback if configured', () => {
expect(
normalizeStatusCode({status_code: 500})
normalizeStatusCode({status_code: 500, headersSent: true})
).toBe(500);
});
it('returns 499 if headers are not sent', () => {
expect(normalizeStatusCode({statusCode: 200, headersSent: false})).toBe(499);
});
});

View File

@@ -68,7 +68,8 @@ function main(opts) {
formatStatusCode: main.normalizeStatusCode,
metricType: 'histogram',
promClient: {},
promRegistry: promClient.register
promRegistry: promClient.register,
metricsApp: null,
}, opts
);
@@ -111,7 +112,8 @@ function main(opts) {
percentiles: opts.percentiles || [0.5, 0.75, 0.95, 0.98, 0.99, 0.999],
maxAgeSeconds: opts.maxAgeSeconds,
ageBuckets: opts.ageBuckets,
registers: [opts.promRegistry]
registers: [opts.promRegistry],
pruneAgedBuckets: opts.pruneAgedBuckets
});
} else if (opts.metricType === 'histogram' || !opts.metricType) {
return new promClient.Histogram({
@@ -131,8 +133,12 @@ function main(opts) {
};
if (opts.includeUp !== false) {
let prefix = '';
if (opts.promClient && opts.promClient.collectDefaultMetrics) {
prefix = opts.promClient.collectDefaultMetrics.prefix || '';
}
metrics.up = new promClient.Gauge({
name: 'up',
name: `${prefix}up`,
help: '1 = up, 0 = not up',
registers: [opts.promRegistry]
});
@@ -140,33 +146,44 @@ function main(opts) {
}
const metricsMiddleware = function(req, res, next) {
res.writeHead(200, {'Content-Type': 'text/plain'});
const sendSuccesss = (output) => {
res.writeHead(200, {'Content-Type': 'text/plain'});
res.end(output);
};
const metricsResponse = opts.promRegistry.metrics();
// starting from prom-client@13 .metrics() returns a Promise
if (metricsResponse.then) {
metricsResponse
.then(output => res.end(output))
.then(output => sendSuccesss(output))
.catch(err => next(err));
} else {
// compatibility fallback for previous versions of prom-client@<=12
res.end(metricsResponse);
sendSuccesss(metricsResponse);
}
};
const metricsMatch = opts.metricsPath instanceof RegExp ? opts.metricsPath
: new RegExp('^' + (opts.metricsPath || '/metrics') + '/?$');
if (typeof opts.bypass === 'function') {
opts.bypass = {
onRequest: opts.bypass
};
} else if (!opts.bypass) {
opts.bypass = {};
}
const middleware = function (req, res, next) {
const path = req.originalUrl || req.url; // originalUrl gets lost in koa-connect?
if (opts.autoregister && path.match(metricsMatch)) {
return metricsMiddleware(req, res);
return metricsMiddleware(req, res, next);
}
// bypass() is checked only after /metrics was processed
// if you wish to disable /metrics use autoregister:false instead
if (opts.bypass && opts.bypass(req)) {
if (opts.bypass.onRequest && opts.bypass.onRequest(req)) {
return next();
}
@@ -178,6 +195,10 @@ function main(opts) {
const timer = metrics[httpMetricName].startTimer(labels);
onFinished(res, () => {
if (opts.bypass.onFinish && opts.bypass.onFinish(req, res)) {
return;
}
if (opts.includeStatusCode) {
labels.status_code = opts.formatStatusCode(res, opts);
}
@@ -201,6 +222,14 @@ function main(opts) {
next();
};
if (opts.metricsApp) {
opts.metricsApp.get(opts.metricsPath || '/metrics', (req, res) => {
res.set('Content-Type', opts.promRegistry.contentType);
opts.promRegistry.metrics()
.then(metrics => res.end(metrics));
});
}
middleware.metrics = metrics;
middleware.promClient = promClient;
middleware.metricsMiddleware = metricsMiddleware;

View File

@@ -11,6 +11,7 @@ module.exports = function(req, opts) {
// by middlewares such as 'router'. Note: this function is called onFinish
/// i.e. always in the tail of the middleware chain
let path = url.parse(req.originalUrl || req.url).pathname;
const urlPathReplacement = opts ? opts.urlPathReplacement : '#val';
const normalizePath = opts && opts.normalizePath;
if (Array.isArray(normalizePath)) {
@@ -26,5 +27,5 @@ module.exports = function(req, opts) {
if (!urlValueParser) {
urlValueParser = new UrlValueParser(opts && opts.urlValueParser);
}
return urlValueParser.replacePathValues(path);
return urlValueParser.replacePathValues(path, urlPathReplacement);
};

View File

@@ -1,5 +1,11 @@
'use strict';
const CLIENT_CLOSED_REQUEST_CODE = 499;
module.exports = function(res) {
return res.status_code || res.statusCode;
if (res.headersSent) {
return res.status_code || res.statusCode;
} else {
return CLIENT_CLOSED_REQUEST_CODE;
}
};

42
types/index.d.ts vendored
View File

@@ -1,7 +1,7 @@
// TypeScript Version: 2.8
import { Request, RequestHandler, Response } from 'express';
import { DefaultMetricsCollectorConfiguration, Registry } from 'prom-client';
import { Request, RequestHandler, Response, Express } from 'express';
import { DefaultMetricsCollectorConfiguration, Registry, RegistryContentType } from 'prom-client';
export {};
@@ -17,9 +17,8 @@ declare namespace express_prom_bundle {
type NormalizeStatusCodeFn = (res: Response) => number | string;
type TransformLabelsFn = (labels: Labels, req: Request, res: Response) => void;
interface Opts {
interface BaseOptions {
autoregister?: boolean;
buckets?: number[];
customLabels?: { [key: string]: any };
@@ -28,26 +27,51 @@ declare namespace express_prom_bundle {
includePath?: boolean;
includeUp?: boolean;
bypass?: (req: Request) => boolean;
bypass?:
| ((req: Request) => boolean)
| {
onRequest?: (req: Request) => boolean;
onFinish?: (req: Request, res: Response) => boolean;
};
excludeRoutes?: Array<string | RegExp>;
metricType?: 'summary' | 'histogram';
metricsPath?: string;
httpDurationMetricName?: string;
promClient?: { collectDefaultMetrics?: DefaultMetricsCollectorConfiguration };
promClient?: { collectDefaultMetrics?: DefaultMetricsCollectorConfiguration<RegistryContentType> };
promRegistry?: Registry;
normalizePath?: NormalizePathEntry[] | NormalizePathFn;
formatStatusCode?: NormalizeStatusCodeFn;
transformLabels?: TransformLabelsFn;
urlPathReplacement?: string;
metricsApp?: Express;
// https://github.com/disjunction/url-value-parser#options
urlValueParser?: {
minHexLength?: number;
minBase64Length?: number;
replaceMasks?: string[];
extraMasks?: string[];
replaceMasks?: Array<RegExp | string>;
extraMasks?: Array<RegExp | string>;
};
}
/** @see https://github.com/siimon/prom-client#summary */
type SummaryOptions = BaseOptions & {
metricType?: 'summary';
percentiles?: number[];
maxAgeSeconds?: number;
ageBuckets?: number;
pruneAgedBuckets?: boolean;
}
/** @see https://github.com/siimon/prom-client#histogram */
type HistogramOptions = BaseOptions & {
metricType?: 'histogram';
buckets?: number[];
}
type Opts = SummaryOptions | HistogramOptions;
interface Middleware extends RequestHandler {
metricsMiddleware: RequestHandler;
}

View File

@@ -1,18 +1,17 @@
import { Request, RequestHandler, Response } from 'express';
import express, { RequestHandler } from 'express';
import { expectType } from 'tsd'
import * as promClient from 'prom-client';
import promBundle, {
type Middleware
} from '..';
import * as promBundle from 'express-prom-bundle';
const middleware: express.RequestHandler = promBundle({ includeMethod: true });
// $ExpectType Middleware
const middleware: RequestHandler = promBundle({ includeMethod: true });
// $ExpectType: string
middleware.name;
expectType<string>(middleware.name);
promClient.register.clear();
// $ExpectType Middleware
promBundle({
expectType<Middleware>(promBundle({
normalizePath: [
// collect paths like "/customer/johnbobson" as just one "/custom/#name"
['^/customer/.*', '/customer/#name'],
@@ -26,21 +25,21 @@ promBundle({
'ORD[0-9]{5,}' // replace strings like ORD1243423, ORD673562 as #val
]
}
});
}));
promClient.register.clear();
// $ExpectType Middleware
promBundle({
expectType<Middleware>(promBundle({
buckets: [0.1, 0.4, 0.7],
includeMethod: true,
includePath: true,
excludeRoutes: ['/foo', /^\/bar\/?$/],
customLabels: { year: null },
transformLabels: (labels: promBundle.Labels) => ({
...labels,
year: new Date().getFullYear()
}),
metricType: 'summary',
metricType: 'histogram',
metricsPath: '/prometheus',
promClient: {
collectDefaultMetrics: {
@@ -56,24 +55,37 @@ promBundle({
normalizePath: [
['^/foo', '/example'] // replace /foo with /example
],
formatStatusCode: (res: Response) => res.statusCode + 100
formatStatusCode: (res: express.Response) => res.statusCode + 100,
metricsApp: express()
}));
promClient.register.clear();
promBundle({
metricType: 'summary',
maxAgeSeconds: 600,
ageBuckets: 5
});
promClient.register.clear();
promBundle({
metricType: 'summary',
percentiles: [0.01, 0.1, 0.9, 0.99]
});
// TypeScript workaround to write a readonly field
type Writable<T> = { -readonly [K in keyof T]: T[K] };
const wPromBundle: Writable<promBundle> = promBundle;
wPromBundle.normalizePath = (req: Request, opts: promBundle.Opts) => {
wPromBundle.normalizePath = (req: express.Request, opts: promBundle.Opts) => {
const path = promBundle.normalizePath(req, opts);
// count all docs as one path, but /docs/login as a separate one
return path.match(/^\/docs/) && !path.match(/^\/login/) ? '/docs/*' : path;
};
wPromBundle.normalizeStatusCode = (res: Response) => res.statusCode.toString();
wPromBundle.normalizeStatusCode = (res: express.Response) => res.statusCode.toString();
// $ExpectType RequestHandler
promBundle.clusterMetrics();
expectType<RequestHandler>(promBundle.clusterMetrics());
// Missing test
// const stringReturn: string = promBundle.normalizePath({}, {});

View File

@@ -1,14 +0,0 @@
{
"compilerOptions": {
"module": "commonjs",
"lib": ["es6"],
"noImplicitAny": true,
"noImplicitThis": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"baseUrl": ".",
"paths": { "express-prom-bundle": ["."] },
"noEmit": true,
"forceConsistentCasingInFileNames": true
}
}

View File

@@ -1,3 +0,0 @@
{
"extends": "dtslint/dtslint.json"
}