mirror of
https://github.com/BreizhHardware/express-prom-bundle.git
synced 2026-01-19 00:37:36 +01:00
Compare commits
209 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f9a0a7622a | ||
|
|
fd33d98c15 | ||
|
|
5978ea7b73 | ||
|
|
71467e6a17 | ||
|
|
c6b24f6eca | ||
|
|
1ac5fba5c4 | ||
|
|
ca8b0ba1e0 | ||
|
|
2ebc2618de | ||
|
|
df46ecaa9a | ||
|
|
abdfe2d93a | ||
|
|
33ab388106 | ||
|
|
02865e531d | ||
|
|
1a1d8e0b54 | ||
|
|
8371c551d5 | ||
|
|
10a58635e1 | ||
|
|
5f44228f69 | ||
|
|
fd5ff1cfe0 | ||
|
|
edfb9992ed | ||
|
|
0ce44722a5 | ||
|
|
407ea4b0d7 | ||
|
|
63b6d89caa | ||
|
|
5e2b284903 | ||
|
|
c693affcac | ||
|
|
a0eef5d0e2 | ||
|
|
efbab7dcdb | ||
|
|
0bda9934c1 | ||
|
|
5bf6b153e6 | ||
|
|
1171fb5be1 | ||
|
|
ea9f34aa3e | ||
|
|
c541824657 | ||
|
|
7eae5d4b7f | ||
|
|
93b0e8fafe | ||
|
|
2d2b253567 | ||
|
|
1b52c6f46e | ||
|
|
2293d1cd40 | ||
|
|
1ee094eb04 | ||
|
|
a1284ec3e5 | ||
|
|
5a1efd7fe6 | ||
|
|
bcac9f523a | ||
|
|
c4be86b651 | ||
|
|
5e5a47a500 | ||
|
|
426e4d4556 | ||
|
|
78ebb06a6d | ||
|
|
c6d2210062 | ||
|
|
c436fa3c88 | ||
|
|
77b02c7302 | ||
|
|
068ed93a61 | ||
|
|
c5aa3f7388 | ||
|
|
ca28ff4fe1 | ||
|
|
52f47ed9f4 | ||
|
|
a60996ebc3 | ||
|
|
e7c94ff307 | ||
|
|
884617bffe | ||
|
|
32b76d0970 | ||
|
|
845cd6ee46 | ||
|
|
0c3c949425 | ||
|
|
1ec800cde7 | ||
|
|
661e7d5092 | ||
|
|
b569a71130 | ||
|
|
10c372c438 | ||
|
|
3599c32876 | ||
|
|
59ec729cb2 | ||
|
|
c95cd1834c | ||
|
|
d18316fee8 | ||
|
|
ccb74f99b0 | ||
|
|
2e093bc14d | ||
|
|
3f6b2746b9 | ||
|
|
83ee0ce06e | ||
|
|
5300d0ef82 | ||
|
|
62abb62772 | ||
|
|
bef92b77e1 | ||
|
|
99d8fc1ea9 | ||
|
|
2478e617bb | ||
|
|
92db62dc90 | ||
|
|
3ffdeef8ae | ||
|
|
7c35f1beb6 | ||
|
|
c44d157cfe | ||
|
|
731fd3ec01 | ||
|
|
3f587fb760 | ||
|
|
3c2779d0d1 | ||
|
|
a3c15b1645 | ||
|
|
f4677ce6c6 | ||
|
|
958453eb91 | ||
|
|
0205d4cfc8 | ||
|
|
bffb4cf16e | ||
|
|
52fdbf030f | ||
|
|
c55d5f4862 | ||
|
|
75b1e2cafa | ||
|
|
e2a8869b7f | ||
|
|
c64772dfc9 | ||
|
|
c1b1022fb5 | ||
|
|
16dfa7f926 | ||
|
|
7e58bd3d06 | ||
|
|
adf15f03f4 | ||
|
|
6629c09dfa | ||
|
|
fe2d1d25ec | ||
|
|
a5d63925ce | ||
|
|
43ba2091d3 | ||
|
|
1909018c17 | ||
|
|
52f3a955df | ||
|
|
e2800e7d30 | ||
|
|
fff0384605 | ||
|
|
514745b23a | ||
|
|
77730b9df5 | ||
|
|
6d9da12d97 | ||
|
|
f1f36f0fb7 | ||
|
|
d2f0088f3c | ||
|
|
4f89424c79 | ||
|
|
33765b3654 | ||
|
|
1a8f38c983 | ||
|
|
23cedb9f05 | ||
|
|
20c6a7de24 | ||
|
|
0a1fda31de | ||
|
|
7654a4ec79 | ||
|
|
faeeb7b57b | ||
|
|
a21bd439cf | ||
|
|
90f2d90e46 | ||
|
|
71c0e52020 | ||
|
|
78e25d6587 | ||
|
|
02f3fe9008 | ||
|
|
2a5e25dd77 | ||
|
|
f770ab800d | ||
|
|
6041bee4dd | ||
|
|
a92b4cfbe6 | ||
|
|
8df2778337 | ||
|
|
b00d88c4a7 | ||
|
|
11988cb025 | ||
|
|
df6854ee04 | ||
|
|
48d883b7f2 | ||
|
|
b835e90560 | ||
|
|
088ff5a063 | ||
|
|
fd12de6ea3 | ||
|
|
0b8706dd42 | ||
|
|
982ff86543 | ||
|
|
5f39d8f357 | ||
|
|
d405e3f584 | ||
|
|
ff147d8fc4 | ||
|
|
43f5a2b04f | ||
|
|
09ee5d954a | ||
|
|
33cca0d2cf | ||
|
|
2040c043fd | ||
|
|
f6466d007b | ||
|
|
e68466d7af | ||
|
|
6b49ffab08 | ||
|
|
cb128c7520 | ||
|
|
d336165848 | ||
|
|
cfe0065146 | ||
|
|
963a66a25e | ||
|
|
389684b426 | ||
|
|
846ccfc641 | ||
|
|
17fe5f4c70 | ||
|
|
66ef4e3176 | ||
|
|
f55d397a31 | ||
|
|
68ad108f77 | ||
|
|
6000056f7a | ||
|
|
48856afbe3 | ||
|
|
e035e2b991 | ||
|
|
c6d5964768 | ||
|
|
94722e908b | ||
|
|
00b8369329 | ||
|
|
59221f891b | ||
|
|
3a0b2caf61 | ||
|
|
e7d004f0cc | ||
|
|
0dd3116f23 | ||
|
|
0606bf5f4f | ||
|
|
6054824c67 | ||
|
|
ec9835270f | ||
|
|
01c78bcc1d | ||
|
|
1cf8c86acb | ||
|
|
43b51d4ab1 | ||
|
|
f6e87b7697 | ||
|
|
3be8d53a27 | ||
|
|
34e6a21be1 | ||
|
|
02fcda4721 | ||
|
|
bc65dc45cb | ||
|
|
8cae5b6ef3 | ||
|
|
2003e7743f | ||
|
|
568c87216a | ||
|
|
98be36244e | ||
|
|
6ff1204db4 | ||
|
|
f71d837660 | ||
|
|
4aa2bfa6ae | ||
|
|
1fff877787 | ||
|
|
5e0cd75673 | ||
|
|
d292dcab33 | ||
|
|
84f99cc49c | ||
|
|
fffe35ce5e | ||
|
|
06341c227a | ||
|
|
45b8f373be | ||
|
|
4ee269faee | ||
|
|
e4d6113ff2 | ||
|
|
06f55c9ab8 | ||
|
|
c8996a7730 | ||
|
|
aed7edc684 | ||
|
|
4a840cfce6 | ||
|
|
d12248dcaf | ||
|
|
68eb617f88 | ||
|
|
bb0e453078 | ||
|
|
b0928b1b94 | ||
|
|
26f9f5ca10 | ||
|
|
a261ab76a3 | ||
|
|
8318e0ec1d | ||
|
|
d58b434f0a | ||
|
|
47fd051aba | ||
|
|
313673f16f | ||
|
|
bbd62e34d6 | ||
|
|
b8ba87009e | ||
|
|
1cc588c2da | ||
|
|
5b1517ca91 |
@@ -17,6 +17,7 @@
|
||||
"extends": "eslint:recommended",
|
||||
|
||||
"rules": {
|
||||
"indent": [1, 2],
|
||||
"array-bracket-spacing": [2, "never"],
|
||||
"block-scoped-var": 2,
|
||||
"brace-style": [2, "1tbs"],
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,3 +1,5 @@
|
||||
.npmrc
|
||||
node_modules
|
||||
coverage
|
||||
/.vscode
|
||||
.env
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
advanced-example.js
|
||||
docker-compose.yml
|
||||
spec
|
||||
.travis.yml
|
||||
.eslintrc
|
||||
coverage
|
||||
12
.travis.yml
12
.travis.yml
@@ -1,5 +1,11 @@
|
||||
language: node_js
|
||||
node_js:
|
||||
- "6"
|
||||
- "5"
|
||||
- "4"
|
||||
- "18"
|
||||
notifications:
|
||||
email: false
|
||||
before_install:
|
||||
- npm install prom-client
|
||||
script:
|
||||
- npm run lint
|
||||
- npm test
|
||||
- npm run test-types
|
||||
|
||||
5
Makefile
5
Makefile
@@ -1,9 +1,10 @@
|
||||
.PHONY: coverage
|
||||
|
||||
test:
|
||||
./node_modules/jasme/run.js
|
||||
npm test
|
||||
lint:
|
||||
node_modules/eslint/bin/eslint.js src
|
||||
npx eslint src
|
||||
npm run dtslint-next
|
||||
coverage:
|
||||
node_modules/istanbul/lib/cli.js cover \
|
||||
-i 'src/*' \
|
||||
|
||||
217
README.md
217
README.md
@@ -2,29 +2,29 @@
|
||||
|
||||
# express prometheus bundle
|
||||
|
||||
Express middleware with popular prometheus metrics in one bundle. It's also compatible with koa v1 (see below).
|
||||
Express middleware with popular prometheus metrics in one bundle. It's also compatible with koa v1 and v2 (see below).
|
||||
|
||||
Internally it uses **prom-client**. 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:
|
||||
|
||||
* `up`: normally is just 1
|
||||
* `http_request_duration_seconds`: http latency histogram labeled with `status_code`, `method` and `path`
|
||||
|
||||
**Please note version 2.x is NOT backwards compatible with 1.x**
|
||||
* `http_request_duration_seconds`: http latency histogram/summary labeled with `status_code`, `method` and `path`
|
||||
|
||||
## Install
|
||||
|
||||
```
|
||||
npm install express-prom-bundle
|
||||
npm install prom-client express-prom-bundle
|
||||
```
|
||||
|
||||
## Usage
|
||||
## Sample Usage
|
||||
|
||||
```javascript
|
||||
const promBundle = require("express-prom-bundle");
|
||||
const metricsMiddleware = promBundle({/* options */ });
|
||||
const app = require("express")();
|
||||
const metricsMiddleware = promBundle({includeMethod: true});
|
||||
|
||||
app.use(metricsMiddleware);
|
||||
app.use(/* your middleware */);
|
||||
@@ -44,40 +44,116 @@ See the example below.
|
||||
|
||||
## Options
|
||||
|
||||
* **buckets**: buckets used for `http_request_seconds` histogram
|
||||
* **includeMethod**: include HTTP method (GET, PUT, ...) as a label to `http_request_duration_seconds`
|
||||
* **includePath**: include URL path as a label (see below)
|
||||
* **normalizePath**: boolean or `function(req)` - path normalization for `includePath` option
|
||||
* **autoregister**: if `/metrics` endpoint should be registered. (Default: **true**)
|
||||
* **whitelist**, **blacklist**: array of strings or regexp specifying which metrics to include/exclude
|
||||
* **excludeRoutes**: (deprecated) array of strings or regexp specifying which routes should be skipped for `http_request_duration_seconds` metric. It uses `req.originalUrl` as subject when checking. You want normally use express or meddleware features instead of this options.
|
||||
Which labels to include in `http_request_duration_seconds` metric:
|
||||
|
||||
* **includeStatusCode**: HTTP status code (200, 400, 404 etc.), default: **true**
|
||||
* **includeMethod**: HTTP method (GET, PUT, ...), default: **false**
|
||||
* **includePath**: URL path (see important details below), default: **false**
|
||||
* **customLabels**: an object containing extra labels, e.g. ```{project_name: 'hello_world'}```.
|
||||
Most useful together with **transformLabels** callback, otherwise it's better to use native Prometheus relabeling.
|
||||
* **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
|
||||
* **httpDurationMetricName**: Allows you change the name of HTTP duration metric, default: **`http_request_duration_seconds`**.
|
||||
|
||||
### metricType option ###
|
||||
|
||||
Two metric types are supported for `http_request_duration_seconds` metric:
|
||||
* [histogram](https://prometheus.io/docs/concepts/metric_types/#histogram) (default)
|
||||
* [summary](https://prometheus.io/docs/concepts/metric_types/#summary)
|
||||
|
||||
Additional options for **histogram**:
|
||||
* **buckets**: buckets used for the `http_request_duration_seconds` histogram
|
||||
|
||||
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 ###
|
||||
|
||||
* **normalizePath**: `function(req)` or `Array`
|
||||
* if function is provided, then it should generate path value from express `req`
|
||||
* if array is provided, then it should be an array of tuples `[regex, replacement]`. The `regex` can be a string and is automatically converted into JS regex.
|
||||
* ... see more details in the section below
|
||||
* **urlValueParser**: options passed when instantiating [url-value-parser](https://github.com/disjunction/url-value-parser).
|
||||
This is the easiest way to customize which parts of the URL should be replaced with "#val".
|
||||
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**)
|
||||
* **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
|
||||
|
||||
The goal is to have separate latency statistics by URL path, e.g. `/my-app/user/`, `/products/by-category` etc.
|
||||
Let's say you want to have latency statistics by URL path,
|
||||
e.g. separate metrics for `/my-app/user/`, `/products/by-category` etc.
|
||||
|
||||
Just taking `req.path` as a label value won't work as IDs are often part of the URL,
|
||||
like `/user/12352/profile`. So what we actually need is a path template.
|
||||
The module tries to figure out what parts of the path are values or IDs,
|
||||
and what is an actual path. The example mentioned before would be
|
||||
normalized to `/user/#val/profile` and that will become the value for the label.
|
||||
These conversions are handled by `normalizePath` function.
|
||||
|
||||
You can override this magical behavior and define your own function by
|
||||
providing an optional callback using **normalizePath** option.
|
||||
You can also replace the default **normalizePath** function globally.
|
||||
This is handy if the rest of the middleware is done elsewhere
|
||||
e.g. via `kraken.js meddleware`.
|
||||
You can extend this magical behavior by providing
|
||||
additional RegExp rules to be performed,
|
||||
or override `normalizePath` with your own function.
|
||||
|
||||
#### Example 1 (add custom RegExp):
|
||||
|
||||
```javascript
|
||||
app.use(promBundle({
|
||||
normalizePath: [
|
||||
// collect paths like "/customer/johnbobson" as just one "/custom/#name"
|
||||
['^/customer/.*', '/customer/#name'],
|
||||
|
||||
// collect paths like "/bobjohnson/order-list" as just one "/#name/order-list"
|
||||
['^.*/order-list', '/#name/order-list']
|
||||
],
|
||||
urlValueParser: {
|
||||
minHexLength: 5,
|
||||
extraMasks: [
|
||||
'ORD[0-9]{5,}' // replace strings like ORD1243423, ORD673562 as #val
|
||||
]
|
||||
}
|
||||
}));
|
||||
```
|
||||
|
||||
#### Example 2 (override normalizePath function):
|
||||
|
||||
```javascript
|
||||
app.use(promBundle(/* options? */));
|
||||
|
||||
// let's reuse the existing one and just add some
|
||||
// functionality on top
|
||||
const originalNormalize = promBunle.normalizePath;
|
||||
promBunle.normalizePath = (req, opts) => {
|
||||
const originalNormalize = promBundle.normalizePath;
|
||||
promBundle.normalizePath = (req, opts) => {
|
||||
const path = originalNormalize(req, opts);
|
||||
// count all docs (no matter which file) as a single path
|
||||
return path.match(/^\/docs/) ? '/docs/*' : path;
|
||||
// count all docs as one path, but /docs/login as a separate one
|
||||
return (path.match(/^\/docs/) && !path.match(/^\/login/)) ? '/docs/*' : path;
|
||||
};
|
||||
```
|
||||
|
||||
@@ -86,6 +162,16 @@ 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
|
||||
|
||||
@@ -116,24 +202,64 @@ app.listen(3000);
|
||||
|
||||
See an [advanced example on github](https://github.com/jochen-schweizer/express-prom-bundle/blob/master/advanced-example.js)
|
||||
|
||||
## koa v1 example
|
||||
## koa v2 example
|
||||
|
||||
```javascript
|
||||
const promBundle = require("express-prom-bundle");
|
||||
const koa = require("koa");
|
||||
const Koa = require("koa");
|
||||
const c2k = require("koa-connect");
|
||||
const metricsMiddleware = promBundle({/* options */ });
|
||||
|
||||
const app = koa();
|
||||
const app = new Koa();
|
||||
|
||||
app.use(c2k(metricsMiddleware));
|
||||
app.use(/* your middleware */);
|
||||
app.listen(3000);
|
||||
```
|
||||
|
||||
## using with cluster
|
||||
|
||||
You'll need to use an additional **clusterMetrics()** middleware.
|
||||
|
||||
In the example below the master process will expose an API with a single endpoint `/metrics`
|
||||
which returns an aggregate of all metrics from all the workers.
|
||||
|
||||
``` javascript
|
||||
const cluster = require('cluster');
|
||||
const promBundle = require('express-prom-bundle');
|
||||
const promClient = require('prom-client');
|
||||
const numCPUs = Math.max(2, require('os').cpus().length);
|
||||
const express = require('express');
|
||||
|
||||
if (cluster.isMaster) {
|
||||
for (let i = 1; i < numCPUs; i++) {
|
||||
cluster.fork();
|
||||
}
|
||||
|
||||
const metricsApp = express();
|
||||
metricsApp.use('/metrics', promBundle.clusterMetrics());
|
||||
metricsApp.listen(9999);
|
||||
|
||||
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
|
||||
includeMethod: true
|
||||
}));
|
||||
app.use((req, res) => res.send(`hello from pid ${process.pid}\n`));
|
||||
app.listen(3000);
|
||||
console.log(`worker ${process.pid} listening on 3000`);
|
||||
}
|
||||
```
|
||||
|
||||
## using with kraken.js
|
||||
|
||||
Here is meddleware config sample, which can be used in a standard **kraken.js** application:
|
||||
Here is meddleware config sample, which can be used in a standard **kraken.js** application.
|
||||
In this case the stats for URI paths and HTTP methods are collected separately,
|
||||
while replacing all HEX values starting from 5 characters and all IP addresses in the path as #val.
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -146,7 +272,18 @@ Here is meddleware config sample, which can be used in a standard **kraken.js**
|
||||
"arguments": [
|
||||
{
|
||||
"includeMethod": true,
|
||||
"buckets": [0.1, 1, 5]
|
||||
"includePath": true,
|
||||
"buckets": [0.1, 1, 5],
|
||||
"promClient": {
|
||||
"collectDefaultMetrics": {
|
||||
}
|
||||
},
|
||||
"urlValueParser": {
|
||||
"minHexLength": 5,
|
||||
"extraMasks": [
|
||||
"^[0-9]+\\.[0-9]+\\.[0-9]+\\.[0-9]+$"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -155,26 +292,6 @@ Here is meddleware config sample, which can be used in a standard **kraken.js**
|
||||
}
|
||||
```
|
||||
|
||||
## Changelog
|
||||
|
||||
* **2.1.0**
|
||||
* deprecate **excludeRoutes**, use **req.originalPath** instead of **req.path**
|
||||
* **2.0.0**
|
||||
* the reason for the version lift were:
|
||||
* compliance to official naming recommendation: https://prometheus.io/docs/practices/naming/
|
||||
* stopping promotion of an anti-pattern - see https://groups.google.com/d/msg/prometheus-developers/XjlOnDCK9qc/ovKzV3AIBwAJ
|
||||
* dealing with **prom-client** being a singleton with a built-in registry
|
||||
* main histogram metric renamed from `http_request_seconds` to `http_request_duration_seconds`
|
||||
* options removed: **prefix**, **keepDefaultMetrics**
|
||||
* factory removed (as the only reason of it was adding the prefix)
|
||||
* upgrade prom-client to 6.3.0
|
||||
* code style changed to the one closer to express
|
||||
|
||||
|
||||
* **1.2.1**
|
||||
* upgrade prom-client to 6.1.2
|
||||
* add options: includeMethod, includePath, keepDefaultMetrics
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
|
||||
@@ -2,24 +2,38 @@
|
||||
|
||||
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');
|
||||
|
||||
// here we want to remove default metrics provided in prom-client
|
||||
// this must be done before initializing promBundle
|
||||
clearInterval(promBundle.promClient.defaultMetrics());
|
||||
promBundle.promClient.register.clear();
|
||||
|
||||
const bundle = promBundle({
|
||||
blacklist: [/up/],
|
||||
buckets: [0.1, 0.4, 0.7],
|
||||
includeMethod: true,
|
||||
includePath: true
|
||||
includePath: true,
|
||||
customLabels: {year: null},
|
||||
transformLabels: labels => Object.assign(labels, {year: new Date().getFullYear()}),
|
||||
metricsPath: '/prometheus',
|
||||
promClient: {
|
||||
collectDefaultMetrics: {
|
||||
}
|
||||
},
|
||||
urlValueParser: {
|
||||
minHexLength: 5,
|
||||
extraMasks: [
|
||||
"^[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: [
|
||||
['^/foo', '/example'] // replace /foo with /example
|
||||
]
|
||||
});
|
||||
|
||||
app.use(bundle);
|
||||
|
||||
// native prom-client metric (no prefix)
|
||||
const c1 = new bundle.promClient.Counter('c1', 'c1 help');
|
||||
const c1 = new promClient.Counter({name: 'c1', help: 'c1 help'});
|
||||
c1.inc(10);
|
||||
|
||||
app.get('/foo/:id', (req, res) => {
|
||||
@@ -35,10 +49,13 @@ app.delete('/foo/:id', (req, res) => {
|
||||
app.get('/bar', (req, res) => res.send('bar response\n'));
|
||||
|
||||
app.listen(3000, () => console.info( // eslint-disable-line
|
||||
'listening on 3000\n'
|
||||
+ 'test in shell console\n\n'
|
||||
+ 'curl localhost:3000/foo/1234\n'
|
||||
+ 'curl -X DELETE localhost:3000/foo/5432\n'
|
||||
+ 'curl localhost:3000/bar\n'
|
||||
+ 'curl localhost:3000/metrics\n'
|
||||
`listening on 3000
|
||||
test in shell console:
|
||||
|
||||
curl localhost:3000/foo/1234
|
||||
curl localhost:3000/foo/09.08.2018
|
||||
curl -X DELETE localhost:3000/foo/5432
|
||||
curl localhost:3000/bar
|
||||
curl localhost:3000/prometheus
|
||||
`
|
||||
));
|
||||
|
||||
4740
package-lock.json
generated
Normal file
4740
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
41
package.json
41
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "express-prom-bundle",
|
||||
"version": "2.2.0",
|
||||
"version": "8.0.0",
|
||||
"description": "express middleware with popular prometheus metrics in one bundle",
|
||||
"main": "src/index.js",
|
||||
"keywords": [
|
||||
@@ -10,29 +10,46 @@
|
||||
"path",
|
||||
"method"
|
||||
],
|
||||
"files": [
|
||||
"src",
|
||||
"types/index.d.ts"
|
||||
],
|
||||
"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",
|
||||
"test-types": "tsd"
|
||||
},
|
||||
"author": "Konstantin Pogorelov <or@pluseq.com>",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/express": "^5.0.0",
|
||||
"on-finished": "^2.3.0",
|
||||
"prom-client": "^6.3.0",
|
||||
"url-value-parser": "^1.0.0"
|
||||
"url-value-parser": "^2.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"coveralls": "^2.11.15",
|
||||
"eslint": "^3.11.1",
|
||||
"express": "^4.14.0",
|
||||
"dts": "^0.1.1",
|
||||
"eslint": "^5.11.0",
|
||||
"express": "^5.0.1",
|
||||
"istanbul": "^0.4.5",
|
||||
"jasme": "^5.2.0",
|
||||
"koa": "^1.2.4",
|
||||
"koa-connect": "^1.0.0",
|
||||
"supertest": "^2.0.1",
|
||||
"supertest-koa-agent": "^0.3.0"
|
||||
"jasme": "^6.0.0",
|
||||
"koa": "^2.6.2",
|
||||
"koa-connect": "^2.0.1",
|
||||
"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": ">=15.0.0"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/jochen-schweizer/express-prom-bundle.git"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,10 +4,25 @@
|
||||
const express = require('express');
|
||||
const supertest = require('supertest');
|
||||
const bundle = require('../');
|
||||
const koa = require('koa');
|
||||
const Koa = require('koa');
|
||||
const c2k = require('koa-connect');
|
||||
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(() => {
|
||||
@@ -17,7 +32,7 @@ describe('index', () => {
|
||||
it('metrics returns up=1', done => {
|
||||
const app = express();
|
||||
const bundled = bundle({
|
||||
whitelist: ['up']
|
||||
excludeRoutes: ['/irrelevant', /at.all/]
|
||||
});
|
||||
app.use(bundled);
|
||||
app.use('/test', (req, res) => res.send('it worked'));
|
||||
@@ -34,12 +49,83 @@ describe('index', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('metrics should be attached to /metrics by default', done => {
|
||||
it('"up"-metric can be excluded', done => {
|
||||
const app = express();
|
||||
const bundled = bundle({
|
||||
whitelist: ['up']
|
||||
includeUp: false
|
||||
});
|
||||
app.use(bundled);
|
||||
app.use('/test', (req, res) => res.send('it worked'));
|
||||
|
||||
const agent = supertest(app);
|
||||
agent.get('/test').end(() => {
|
||||
agent
|
||||
.get('/metrics')
|
||||
.end((err, res) => {
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.text).not.toMatch(/up\s1/);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('metrics path can be defined with a regex', done => {
|
||||
const app = express();
|
||||
const bundled = bundle({
|
||||
metricsPath: /^\/prometheus$/
|
||||
});
|
||||
app.use(bundled);
|
||||
app.use('/test', (req, res) => res.send('it worked'));
|
||||
|
||||
const agent = supertest(app);
|
||||
agent.get('/test').end(() => {
|
||||
agent
|
||||
.get('/prometheus')
|
||||
.end((err, res) => {
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.text).toMatch(/up\s1/);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('metrics path can be defined as regexp', done => {
|
||||
const app = express();
|
||||
const bundled = bundle();
|
||||
app.use(bundled);
|
||||
app.use('/test', (req, res) => res.send('it worked'));
|
||||
|
||||
const agent = supertest(app);
|
||||
agent.get('/test').end(() => {
|
||||
agent
|
||||
.get('/metrics')
|
||||
.end((err, res) => {
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.text).toMatch(/up\s1/);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('httpDurationMetricName overrides histogram metric name', done => {
|
||||
const app = express();
|
||||
const bundled = bundle({
|
||||
httpDurationMetricName: 'my_http_duration'
|
||||
});
|
||||
app.use(bundled);
|
||||
|
||||
const agent = supertest(app);
|
||||
agent.get('/metrics')
|
||||
.end((err, res) => {
|
||||
expect(res.text).toMatch(/my_http_duration/m);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('metrics should be attached to /metrics by default', done => {
|
||||
const app = express();
|
||||
const bundled = bundle();
|
||||
app.use(bundled);
|
||||
|
||||
const agent = supertest(app);
|
||||
agent.get('/metrics')
|
||||
@@ -67,27 +153,6 @@ describe('index', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('metrics can be filtered using exect match', () => {
|
||||
const instance = bundle({blacklist: ['up']});
|
||||
expect(instance.metrics.up).not.toBeDefined();
|
||||
expect(instance.metrics.http_request_duration_seconds).toBeDefined();
|
||||
});
|
||||
it('metrics can be filtered using regex', () => {
|
||||
const instance = bundle({blacklist: [/http/]});
|
||||
expect(instance.metrics.up).toBeDefined();
|
||||
expect(instance.metrics.http_request_duration_seconds).not.toBeDefined();
|
||||
});
|
||||
it('metrics can be whitelisted', () => {
|
||||
const instance = bundle({whitelist: [/^up$/]});
|
||||
expect(instance.metrics.up).toBeDefined();
|
||||
expect(instance.metrics.nodejs_memory_heap_total_bytes).not.toBeDefined();
|
||||
expect(instance.metrics.http_request_duration_seconds).not.toBeDefined();
|
||||
});
|
||||
it('throws on both white and blacklist', () => {
|
||||
expect(() => {
|
||||
bundle({whitelist: [/up/], blacklist: [/up/]});
|
||||
}).toThrow();
|
||||
});
|
||||
it('returns error 500 on incorrect middleware usage', done => {
|
||||
const app = express();
|
||||
app.use(bundle);
|
||||
@@ -107,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')
|
||||
@@ -124,35 +188,119 @@ describe('index', () => {
|
||||
it('filters out the excludeRoutes', done => {
|
||||
const app = express();
|
||||
const instance = bundle({
|
||||
excludeRoutes: ['/test']
|
||||
excludeRoutes: ['/test', /bad.word/]
|
||||
});
|
||||
app.use(instance);
|
||||
app.use('/test', (req, res) => res.send('it worked'));
|
||||
app.use('/some/bad-word', (req, res) => res.send('it worked too'));
|
||||
const agent = supertest(app);
|
||||
agent
|
||||
.get('/test')
|
||||
.end(() => {
|
||||
const metricHashMap = instance.metrics.http_request_duration_seconds.hashMap;
|
||||
expect(metricHashMap['status_code:200']).not.toBeDefined();
|
||||
|
||||
agent
|
||||
.get('/metrics')
|
||||
.end((err, res) => {
|
||||
expect(res.status).toBe(200);
|
||||
done();
|
||||
.get('/some/bad-word')
|
||||
.end(() => {
|
||||
const metricHashMap = instance.metrics.http_request_duration_seconds.hashMap;
|
||||
expect(metricHashMap['status_code:200']).not.toBeDefined();
|
||||
|
||||
agent
|
||||
.get('/metrics')
|
||||
.end((err, res) => {
|
||||
expect(res.status).toBe(200);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('bypass requests', done => {
|
||||
const app = express();
|
||||
const instance = bundle({
|
||||
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/);
|
||||
}
|
||||
});
|
||||
app.use(instance);
|
||||
app.use('/test', (req, res) => res.send('it worked'));
|
||||
app.use('/some/bad-word', (req, res) => res.send('it worked too'));
|
||||
app.use('/good-word', (req, res) => res.send('this will be counted'));
|
||||
const agent = supertest(app);
|
||||
agent
|
||||
.get('/test')
|
||||
.end(() => {
|
||||
agent
|
||||
.get('/some/bad-word')
|
||||
.end(() => {
|
||||
agent
|
||||
.get('/good-word')
|
||||
.end(() => {
|
||||
const bucket = extractBucket(instance, 'status_code:200');
|
||||
expect(bucket).toBeDefined();
|
||||
|
||||
// only /good-word should be counted
|
||||
expect(bucket.count).toBe(1);
|
||||
|
||||
agent
|
||||
.get('/metrics')
|
||||
.end((err, res) => {
|
||||
expect(res.status).toBe(200);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
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();
|
||||
});
|
||||
|
||||
it('tolerates includePath, includeMethod', done => {
|
||||
it('tolerates includePath, includeMethod, includeCustomLabels', done => {
|
||||
const app = express();
|
||||
const instance = bundle({
|
||||
includePath: true,
|
||||
includeMethod: true
|
||||
includeMethod: true,
|
||||
includeCustomLabels: {foo: 'bar'}
|
||||
});
|
||||
app.use(instance);
|
||||
app.use('/test', (req, res) => res.send('it worked'));
|
||||
@@ -169,12 +317,145 @@ describe('index', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('normalizePath can be replaced', done => {
|
||||
it('metric type histogram works', done => {
|
||||
const app = express();
|
||||
const bundled = bundle({
|
||||
metricType: 'histogram',
|
||||
buckets: [10, 100],
|
||||
});
|
||||
app.use(bundled);
|
||||
app.use('/test', (req, res) => res.send('it worked'));
|
||||
|
||||
const agent = supertest(app);
|
||||
agent.get('/test').end(() => {
|
||||
agent
|
||||
.get('/metrics')
|
||||
.end((err, res) => {
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.text).toMatch(/le="100"/);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('throws on unknown metricType ', () => {
|
||||
expect(() => {
|
||||
bundle({metricType: 'hello'});
|
||||
}).toThrow();
|
||||
});
|
||||
|
||||
describe('usage of normalizePath()', () => {
|
||||
|
||||
it('normalizePath can be replaced globally', done => {
|
||||
const app = express();
|
||||
const original = bundle.normalizePath;
|
||||
bundle.normalizePath = () => 'dummy';
|
||||
const instance = bundle({
|
||||
includePath: true,
|
||||
});
|
||||
app.use(instance);
|
||||
app.use('/test', (req, res) => res.send('it worked'));
|
||||
const agent = supertest(app);
|
||||
agent
|
||||
.get('/test')
|
||||
.end(() => {
|
||||
agent
|
||||
.get('/metrics')
|
||||
.end((err, res) => {
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.text).toMatch(/"dummy"/m);
|
||||
bundle.normalizePath = original;
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
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({
|
||||
includePath: true,
|
||||
normalizePath: req => req.originalUrl + '-suffixed'
|
||||
});
|
||||
app.use(instance);
|
||||
app.use('/test', (req, res) => res.send('it worked'));
|
||||
const agent = supertest(app);
|
||||
agent
|
||||
.get('/test')
|
||||
.end(() => {
|
||||
agent
|
||||
.get('/metrics')
|
||||
.end((err, res) => {
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.text).toMatch(/"\/test-suffixed"/m);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('normalizePath can be passed as an array of [regex, replacement]', done => {
|
||||
const app = express();
|
||||
const instance = bundle({
|
||||
includePath: true,
|
||||
normalizePath: [
|
||||
['^/docs/whatever/.*$', '/docs'],
|
||||
[/^\/docs$/, '/mocks']
|
||||
]
|
||||
});
|
||||
app.use(instance);
|
||||
app.use('/docs/whatever/:andmore', (req, res) => res.send('it worked'));
|
||||
const agent = supertest(app);
|
||||
agent
|
||||
.get('/docs/whatever/unimportant')
|
||||
.end(() => {
|
||||
agent
|
||||
.get('/metrics')
|
||||
.end((err, res) => {
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.text).toMatch(/"\/mocks"/m);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('formatStatusCode can be overridden', done => {
|
||||
const app = express();
|
||||
const original = bundle.normalizePath;
|
||||
bundle.normalizePath = () => 'dummy';
|
||||
const instance = bundle({
|
||||
includePath: true,
|
||||
formatStatusCode: () => 555
|
||||
});
|
||||
app.use(instance);
|
||||
app.use('/test', (req, res) => res.send('it worked'));
|
||||
@@ -186,25 +467,109 @@ describe('index', () => {
|
||||
.get('/metrics')
|
||||
.end((err, res) => {
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.text).toMatch(/"dummy"/m);
|
||||
bundle.normalizePath = original;
|
||||
expect(res.text).toMatch(/555/);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('includeStatusCode=false removes status_code label from metrics', done => {
|
||||
const app = express();
|
||||
const instance = bundle({
|
||||
includeStatusCode: false
|
||||
});
|
||||
app.use(instance);
|
||||
app.use('/test', (req, res) => res.send('it worked'));
|
||||
const agent = supertest(app);
|
||||
agent
|
||||
.get('/test')
|
||||
.end(() => {
|
||||
agent
|
||||
.get('/metrics')
|
||||
.end((err, res) => {
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.text).not.toMatch(/="200"/);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
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({
|
||||
customLabels: {foo: 'bar'}
|
||||
});
|
||||
app.use(instance);
|
||||
app.use('/test', (req, res) => res.send('it worked'));
|
||||
const agent = supertest(app);
|
||||
agent
|
||||
.get('/test')
|
||||
.end(() => {
|
||||
agent
|
||||
.get('/metrics')
|
||||
.end((err, res) => {
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.text).toMatch(/foo="bar"/);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('tarnsformLabels can set label values', done => {
|
||||
const app = express();
|
||||
const instance = bundle({
|
||||
includePath: true,
|
||||
customLabels: {foo: 'bar'},
|
||||
transformLabels: labels => {
|
||||
labels.foo = 'baz';
|
||||
labels.path += '/ok';
|
||||
}
|
||||
});
|
||||
app.use(instance);
|
||||
app.use('/test', (req, res) => res.send('it worked'));
|
||||
const agent = supertest(app);
|
||||
agent
|
||||
.get('/test')
|
||||
.end(() => {
|
||||
agent
|
||||
.get('/metrics')
|
||||
.end((err, res) => {
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.text).toMatch(/foo="baz"/);
|
||||
expect(res.text).toMatch(/path="\/test\/ok"/);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('Koa: metrics returns up=1', done => {
|
||||
const app = koa();
|
||||
const bundled = bundle({
|
||||
whitelist: ['up']
|
||||
});
|
||||
const app = new Koa();
|
||||
const bundled = bundle();
|
||||
app.use(c2k(bundled));
|
||||
|
||||
app.use(function*(next) {
|
||||
if (this.path !== 'test') {
|
||||
return yield next;
|
||||
}
|
||||
this.body = 'it worked';
|
||||
app.use(function(ctx, next) {
|
||||
return next().then(() => ctx.body = 'it worked');
|
||||
});
|
||||
|
||||
const agent = supertestKoa(app);
|
||||
@@ -218,4 +583,137 @@ describe('index', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('usage of clusterMetrics()', () => {
|
||||
it('clusterMetrics returns 200 even without a cluster', (done) => {
|
||||
const app = express();
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
||||
it('clusterMetrics returns 500 in case of an error', (done) => {
|
||||
const app = express();
|
||||
app.use('/cluster_metrics', bundle.clusterMetrics());
|
||||
const agent = supertest(app);
|
||||
|
||||
// create a fake worker, which would not respond in time
|
||||
cluster.workers = [{
|
||||
isConnected: () => true,
|
||||
send: () => {}
|
||||
}];
|
||||
|
||||
const errorSpy = spyOn(console, 'error'); // mute console.error
|
||||
|
||||
agent
|
||||
.get('/cluster_metrics')
|
||||
.end((err, res) => {
|
||||
expect(res.status).toBe(500);
|
||||
expect(errorSpy).toHaveBeenCalled();
|
||||
done();
|
||||
});
|
||||
}, 6000);
|
||||
});
|
||||
|
||||
describe('metricType: summary', () => {
|
||||
it('metric type summary works', done => {
|
||||
const app = express();
|
||||
const bundled = bundle({
|
||||
metricType: 'summary'
|
||||
});
|
||||
app.use(bundled);
|
||||
app.use('/test', (req, res) => res.send('it worked'));
|
||||
|
||||
const agent = supertest(app);
|
||||
agent.get('/test').end(() => {
|
||||
agent
|
||||
.get('/metrics')
|
||||
.end((err, res) => {
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.text).toMatch(/quantile="0.98"/);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('custom pecentiles work', done => {
|
||||
const app = express();
|
||||
const bundled = bundle({
|
||||
metricType: 'summary',
|
||||
percentiles: [0.5, 0.85, 0.99],
|
||||
});
|
||||
app.use(bundled);
|
||||
app.use('/test', (req, res) => res.send('it worked'));
|
||||
|
||||
const agent = supertest(app);
|
||||
agent.get('/test').end(() => {
|
||||
agent
|
||||
.get('/metrics')
|
||||
.end((err, res) => {
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.text).toMatch(/quantile="0.85"/);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,25 +4,33 @@
|
||||
const normalizePath = require('../src/normalizePath');
|
||||
|
||||
describe('normalizePath', () => {
|
||||
it('returns original if disabled in opts', () => {
|
||||
expect(
|
||||
normalizePath({originalUrl: '/a/12345'}, {normalizePath: false})
|
||||
).toBe('/a/12345');
|
||||
});
|
||||
|
||||
it('returns run callback if configured', () => {
|
||||
expect(
|
||||
normalizePath(
|
||||
{originalUrl: '/a/12345'},
|
||||
{
|
||||
normalizePath: req => req.originalUrl + '-ok'
|
||||
}
|
||||
)
|
||||
).toBe('/a/12345-ok');
|
||||
});
|
||||
|
||||
it('uses UrlValueParser by default', () => {
|
||||
expect(normalizePath({originalUrl: '/a/12345'}))
|
||||
expect(normalizePath({url: '/a/12345'}))
|
||||
.toBe('/a/#val');
|
||||
});
|
||||
|
||||
it('uses normalizePath option', () => {
|
||||
const url = '/hello/world/i/am/finally/free!!!';
|
||||
const result = normalizePath({url}, {
|
||||
normalizePath: [
|
||||
['/hello','/goodbye'],
|
||||
['[^/]+$','happy'],
|
||||
]
|
||||
});
|
||||
expect(result).toBe('/goodbye/world/i/am/finally/happy');
|
||||
});
|
||||
|
||||
it('throws error is bad tuples provided as normalizePath', () => {
|
||||
const subject = () => normalizePath({url: '/test'}, {
|
||||
normalizePath: [
|
||||
['/hello','/goodbye', 'test']
|
||||
]
|
||||
});
|
||||
expect(subject).toThrow();
|
||||
});
|
||||
|
||||
it('uses urlPathReplacement when passed to transform the path', () => {
|
||||
expect(normalizePath({url: '/a/12345'}, {urlPathReplacement: ':id'}))
|
||||
.toBe('/a/:id');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,20 +4,13 @@
|
||||
const normalizeStatusCode = require('../src/normalizeStatusCode');
|
||||
|
||||
describe('normalizeStatusCode', () => {
|
||||
it('returns original if disabled in opts', () => {
|
||||
expect(
|
||||
normalizeStatusCode({status_code: 404}, {normalizeStatusCode: false})
|
||||
).toBe(404);
|
||||
});
|
||||
|
||||
it('returns run callback if configured', () => {
|
||||
expect(
|
||||
normalizeStatusCode(
|
||||
{status_code: 500},
|
||||
{
|
||||
formatStatusCode: res => String(res.status_code).slice(0, -2) + 'xx'
|
||||
}
|
||||
)
|
||||
).toBe('5xx');
|
||||
normalizeStatusCode({status_code: 500, headersSent: true})
|
||||
).toBe(500);
|
||||
});
|
||||
|
||||
it('returns 499 if headers are not sent', () => {
|
||||
expect(normalizeStatusCode({statusCode: 200, headersSent: false})).toBe(499);
|
||||
});
|
||||
});
|
||||
|
||||
264
src/index.js
264
src/index.js
@@ -1,11 +1,10 @@
|
||||
'use strict';
|
||||
const onFinished = require('on-finished');
|
||||
const promClient = require('prom-client');
|
||||
const normalizePath = require('./normalizePath');
|
||||
const normalizeStatusCode = require('./normalizeStatusCode');
|
||||
|
||||
function matchVsRegExps(element, regexps) {
|
||||
for (let regexp of regexps) {
|
||||
for (const regexp of regexps) {
|
||||
if (regexp instanceof RegExp) {
|
||||
if (element.match(regexp)) {
|
||||
return true;
|
||||
@@ -17,29 +16,41 @@ function matchVsRegExps(element, regexps) {
|
||||
return false;
|
||||
}
|
||||
|
||||
function filterArrayByRegExps(array, regexps) {
|
||||
return array.filter(element => {
|
||||
return matchVsRegExps(element, regexps);
|
||||
});
|
||||
}
|
||||
function clusterMetrics() {
|
||||
const aggregatorRegistry = new promClient.AggregatorRegistry();
|
||||
|
||||
function prepareMetricNames(opts, metricTemplates) {
|
||||
const names = Object.keys(metricTemplates);
|
||||
if (opts.whitelist) {
|
||||
if (opts.blacklist) {
|
||||
throw new Error('you cannot have whitelist and blacklist at the same time');
|
||||
const metricsMiddleware = function(req, res) {
|
||||
function sendClusterMetrics(clusterMetrics) {
|
||||
res.set('Content-Type', aggregatorRegistry.contentType);
|
||||
res.send(clusterMetrics);
|
||||
}
|
||||
return filterArrayByRegExps(names, opts.whitelist);
|
||||
}
|
||||
if (opts.blacklist) {
|
||||
const blacklisted = filterArrayByRegExps(names, opts.blacklist);
|
||||
return names.filter(name => blacklisted.indexOf(name) === -1);
|
||||
}
|
||||
return names;
|
||||
|
||||
function sendClusterMetricsError(err) {
|
||||
console.error(err);
|
||||
return res.sendStatus(500);
|
||||
}
|
||||
|
||||
// since prom-client@13 clusterMetrics() method doesn't take cb param,
|
||||
// but we provide it anyway, as at this stage it's unknown which version of prom-client is used
|
||||
const response = aggregatorRegistry.clusterMetrics((err, clusterMetrics) => {
|
||||
if (err) {
|
||||
return sendClusterMetricsError(err);
|
||||
}
|
||||
sendClusterMetrics(clusterMetrics);
|
||||
});
|
||||
|
||||
// if we find out that it was a promise and our cb was useless...
|
||||
if (response && response.then) {
|
||||
response
|
||||
.then(result => sendClusterMetrics(result))
|
||||
.catch(err => sendClusterMetricsError(err));
|
||||
}
|
||||
};
|
||||
|
||||
return metricsMiddleware;
|
||||
}
|
||||
|
||||
function main(opts) {
|
||||
opts = Object.assign({autoregister: true}, opts);
|
||||
if (arguments[2] && arguments[1] && arguments[1].send) {
|
||||
arguments[1].status(500)
|
||||
.send('<h1>500 Error</h1>\n'
|
||||
@@ -49,101 +60,186 @@ function main(opts) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (opts.prefix || opts.keepDefaultMetrics !== undefined) {
|
||||
opts = Object.assign(
|
||||
{
|
||||
autoregister: true,
|
||||
includeStatusCode: true,
|
||||
normalizePath: main.normalizePath,
|
||||
formatStatusCode: main.normalizeStatusCode,
|
||||
metricType: 'histogram',
|
||||
promClient: {},
|
||||
promRegistry: promClient.register,
|
||||
metricsApp: null,
|
||||
}, opts
|
||||
);
|
||||
|
||||
if (opts.prefix
|
||||
|| opts.keepDefaultMetrics !== undefined
|
||||
|| opts.whitelist !== undefined
|
||||
|| opts.blacklist !== undefined
|
||||
) {
|
||||
throw new Error(
|
||||
'express-prom-bundle detected obsolete options:'
|
||||
+ 'prefix and/or keepDefaultMetrics. '
|
||||
'express-prom-bundle detected one of the obsolete options: '
|
||||
+ 'prefix, keepDefaultMetrics, whitelist, blacklist. '
|
||||
+ 'Please refer to oficial docs. '
|
||||
+ 'Most likely you upgraded the module without necessary code changes'
|
||||
+ 'Most likely you upgraded the module without the necessary code changes'
|
||||
);
|
||||
}
|
||||
|
||||
const httpMtricName = opts.httpDurationMetricName || 'http_request_duration_seconds';
|
||||
|
||||
const metricTemplates = {
|
||||
'up': () => new promClient.Gauge(
|
||||
'up',
|
||||
'1 = up, 0 = not up'
|
||||
),
|
||||
'http_request_duration_seconds': () => {
|
||||
const labels = ['status_code'];
|
||||
if (opts.includeMethod) {
|
||||
labels.push('method');
|
||||
}
|
||||
if (opts.includePath) {
|
||||
labels.push('path');
|
||||
}
|
||||
const metric = new promClient.Histogram(
|
||||
httpMtricName,
|
||||
'duration histogram of http responses labeled with: ' + labels.join(', '),
|
||||
labels,
|
||||
{
|
||||
buckets: opts.buckets || [0.003, 0.03, 0.1, 0.3, 1.5, 10]
|
||||
}
|
||||
);
|
||||
return metric;
|
||||
}
|
||||
};
|
||||
|
||||
const metrics = {};
|
||||
const names = prepareMetricNames(opts, metricTemplates);
|
||||
|
||||
for (let name of names) {
|
||||
metrics[name] = metricTemplates[name]();
|
||||
if (opts.promClient.collectDefaultMetrics) {
|
||||
promClient.collectDefaultMetrics(opts.promClient.collectDefaultMetrics);
|
||||
}
|
||||
|
||||
if (metrics.up) {
|
||||
const httpMetricName = opts.httpDurationMetricName || 'http_request_duration_seconds';
|
||||
|
||||
function makeHttpMetric() {
|
||||
const labels = ['status_code'];
|
||||
if (opts.includeMethod) {
|
||||
labels.push('method');
|
||||
}
|
||||
if (opts.includePath) {
|
||||
labels.push('path');
|
||||
}
|
||||
if (opts.customLabels) {
|
||||
labels.push.apply(labels, Object.keys(opts.customLabels));
|
||||
}
|
||||
|
||||
if (opts.metricType === 'summary') {
|
||||
return new promClient.Summary({
|
||||
name: httpMetricName,
|
||||
help: 'duration summary of http responses labeled with: ' + labels.join(', '),
|
||||
labelNames: labels,
|
||||
percentiles: opts.percentiles || [0.5, 0.75, 0.95, 0.98, 0.99, 0.999],
|
||||
maxAgeSeconds: opts.maxAgeSeconds,
|
||||
ageBuckets: opts.ageBuckets,
|
||||
registers: [opts.promRegistry],
|
||||
pruneAgedBuckets: opts.pruneAgedBuckets
|
||||
});
|
||||
} else if (opts.metricType === 'histogram' || !opts.metricType) {
|
||||
return new promClient.Histogram({
|
||||
name: httpMetricName,
|
||||
help: 'duration histogram of http responses labeled with: ' + labels.join(', '),
|
||||
labelNames: labels,
|
||||
buckets: opts.buckets || [0.003, 0.03, 0.1, 0.3, 1.5, 10],
|
||||
registers: [opts.promRegistry]
|
||||
});
|
||||
} else {
|
||||
throw new Error('metricType option must be histogram or summary');
|
||||
}
|
||||
}
|
||||
|
||||
const metrics = {
|
||||
[httpMetricName]: makeHttpMetric()
|
||||
};
|
||||
|
||||
if (opts.includeUp !== false) {
|
||||
let prefix = '';
|
||||
if (opts.promClient && opts.promClient.collectDefaultMetrics) {
|
||||
prefix = opts.promClient.collectDefaultMetrics.prefix || '';
|
||||
}
|
||||
metrics.up = new promClient.Gauge({
|
||||
name: `${prefix}up`,
|
||||
help: '1 = up, 0 = not up',
|
||||
registers: [opts.promRegistry]
|
||||
});
|
||||
metrics.up.set(1);
|
||||
}
|
||||
|
||||
const metricsMiddleware = function(req, res) {
|
||||
res.writeHead(200, {'Content-Type': 'text/plain'});
|
||||
res.end(promClient.register.metrics());
|
||||
const metricsMiddleware = function(req, res, next) {
|
||||
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 => sendSuccesss(output))
|
||||
.catch(err => next(err));
|
||||
} else {
|
||||
// compatibility fallback for previous versions of prom-client@<=12
|
||||
sendSuccesss(metricsResponse);
|
||||
}
|
||||
};
|
||||
|
||||
const middleware = function (req, res, next) {
|
||||
const path = req.originalUrl;
|
||||
let labels;
|
||||
const metricsMatch = opts.metricsPath instanceof RegExp ? opts.metricsPath
|
||||
: new RegExp('^' + (opts.metricsPath || '/metrics') + '/?$');
|
||||
|
||||
if (opts.autoregister && path.match(/^\/metrics\/?$/)) {
|
||||
return metricsMiddleware(req, res);
|
||||
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, next);
|
||||
}
|
||||
|
||||
// bypass() is checked only after /metrics was processed
|
||||
// if you wish to disable /metrics use autoregister:false instead
|
||||
if (opts.bypass.onRequest && opts.bypass.onRequest(req)) {
|
||||
return next();
|
||||
}
|
||||
|
||||
if (opts.excludeRoutes && matchVsRegExps(path, opts.excludeRoutes)) {
|
||||
return next();
|
||||
}
|
||||
|
||||
if (metrics[httpMtricName]) {
|
||||
labels = {'status_code': 0};
|
||||
let timer = metrics[httpMtricName].startTimer(labels);
|
||||
onFinished(res, () => {
|
||||
if (opts.normalizeStatusCode) {
|
||||
labels.status_code = main.normalizeStatusCode(res, opts);
|
||||
} else {
|
||||
labels.status_code = res.statusCode;
|
||||
}
|
||||
const labels = {};
|
||||
const timer = metrics[httpMetricName].startTimer(labels);
|
||||
|
||||
if (opts.includeMethod) {
|
||||
labels.method = req.method;
|
||||
}
|
||||
if (opts.includePath) {
|
||||
labels.path = main.normalizePath(req, opts);
|
||||
}
|
||||
timer();
|
||||
});
|
||||
}
|
||||
onFinished(res, () => {
|
||||
if (opts.bypass.onFinish && opts.bypass.onFinish(req, res)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (opts.includeStatusCode) {
|
||||
labels.status_code = opts.formatStatusCode(res, opts);
|
||||
}
|
||||
if (opts.includeMethod) {
|
||||
labels.method = req.method;
|
||||
}
|
||||
if (opts.includePath) {
|
||||
labels.path = opts.normalizePath instanceof Function
|
||||
? opts.normalizePath(req, opts)
|
||||
: main.normalizePath(req, opts);
|
||||
}
|
||||
if (opts.customLabels) {
|
||||
Object.assign(labels, opts.customLabels);
|
||||
}
|
||||
if (opts.transformLabels) {
|
||||
opts.transformLabels(labels, req, res);
|
||||
}
|
||||
timer();
|
||||
});
|
||||
|
||||
next();
|
||||
};
|
||||
|
||||
middleware.metricTemplates = metricTemplates;
|
||||
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;
|
||||
return middleware;
|
||||
}
|
||||
|
||||
// this is kept only for compatibility with the code relying on older version
|
||||
main.promClient = promClient;
|
||||
|
||||
main.normalizePath = normalizePath;
|
||||
main.normalizeStatusCode = normalizeStatusCode;
|
||||
main.clusterMetrics = clusterMetrics;
|
||||
module.exports = main;
|
||||
|
||||
@@ -2,25 +2,30 @@
|
||||
|
||||
const UrlValueParser = require('url-value-parser');
|
||||
const url = require('url');
|
||||
|
||||
// ATTENTION! urlValueParser is a singleton!
|
||||
let urlValueParser;
|
||||
|
||||
module.exports = function(req, opts) {
|
||||
opts = opts || {};
|
||||
|
||||
// originalUrl is taken, because url and path can be changed
|
||||
// by middlewares such as 'router'. Note: this function is called onFinish
|
||||
/// i.e. always in the tail of the middleware chain
|
||||
const path = url.parse(req.originalUrl).pathname;
|
||||
let path = url.parse(req.originalUrl || req.url).pathname;
|
||||
const urlPathReplacement = opts ? opts.urlPathReplacement : '#val';
|
||||
|
||||
if (opts.normalizePath !== undefined && !opts.normalizePath) {
|
||||
return path;
|
||||
}
|
||||
if (typeof opts.normalizePath === 'function') {
|
||||
return opts.normalizePath(req, opts);
|
||||
const normalizePath = opts && opts.normalizePath;
|
||||
if (Array.isArray(normalizePath)) {
|
||||
for (const tuple of normalizePath) {
|
||||
if (!Array.isArray(tuple) || tuple.length !== 2) {
|
||||
throw new Error('Bad tuple provided in normalizePath option, expected: [regex, replacement]');
|
||||
}
|
||||
const regex = typeof tuple[0] === 'string' ? RegExp(tuple[0]) : tuple[0];
|
||||
path = path.replace(regex, tuple[1]);
|
||||
}
|
||||
}
|
||||
|
||||
if (!urlValueParser) {
|
||||
urlValueParser = new UrlValueParser();
|
||||
urlValueParser = new UrlValueParser(opts && opts.urlValueParser);
|
||||
}
|
||||
return urlValueParser.replacePathValues(path);
|
||||
return urlValueParser.replacePathValues(path, urlPathReplacement);
|
||||
};
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = function(res, opts) {
|
||||
opts = opts || {};
|
||||
const CLIENT_CLOSED_REQUEST_CODE = 499;
|
||||
|
||||
if (typeof opts.formatStatusCode === 'function') {
|
||||
return opts.formatStatusCode(res, opts);
|
||||
module.exports = function(res) {
|
||||
if (res.headersSent) {
|
||||
return res.status_code || res.statusCode;
|
||||
} else {
|
||||
return CLIENT_CLOSED_REQUEST_CODE;
|
||||
}
|
||||
|
||||
return res.status_code;
|
||||
};
|
||||
|
||||
90
types/index.d.ts
vendored
Normal file
90
types/index.d.ts
vendored
Normal file
@@ -0,0 +1,90 @@
|
||||
// TypeScript Version: 2.8
|
||||
|
||||
import { Request, RequestHandler, Response, Express } from 'express';
|
||||
import { DefaultMetricsCollectorConfiguration, Registry, RegistryContentType } from 'prom-client';
|
||||
|
||||
export {};
|
||||
|
||||
export = express_prom_bundle;
|
||||
|
||||
declare namespace express_prom_bundle {
|
||||
interface Labels {
|
||||
[key: string]: string | number;
|
||||
}
|
||||
|
||||
type NormalizePathEntry = [string | RegExp, string];
|
||||
type NormalizePathFn = (req: Request, opts: Opts) => string;
|
||||
type NormalizeStatusCodeFn = (res: Response) => number | string;
|
||||
type TransformLabelsFn = (labels: Labels, req: Request, res: Response) => void;
|
||||
|
||||
interface BaseOptions {
|
||||
autoregister?: boolean;
|
||||
|
||||
customLabels?: { [key: string]: any };
|
||||
|
||||
includeStatusCode?: boolean;
|
||||
includeMethod?: boolean;
|
||||
includePath?: boolean;
|
||||
includeUp?: boolean;
|
||||
|
||||
bypass?:
|
||||
| ((req: Request) => boolean)
|
||||
| {
|
||||
onRequest?: (req: Request) => boolean;
|
||||
onFinish?: (req: Request, res: Response) => boolean;
|
||||
};
|
||||
|
||||
excludeRoutes?: Array<string | RegExp>;
|
||||
|
||||
metricsPath?: string;
|
||||
httpDurationMetricName?: string;
|
||||
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?: 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;
|
||||
}
|
||||
|
||||
const normalizePath: NormalizePathFn;
|
||||
const normalizeStatusCode: NormalizeStatusCodeFn;
|
||||
|
||||
function clusterMetrics(): RequestHandler;
|
||||
}
|
||||
|
||||
interface express_prom_bundle {
|
||||
normalizePath: express_prom_bundle.NormalizePathFn;
|
||||
normalizeStatusCode: express_prom_bundle.NormalizeStatusCodeFn;
|
||||
}
|
||||
|
||||
declare function express_prom_bundle(opts: express_prom_bundle.Opts): express_prom_bundle.Middleware;
|
||||
91
types/index.test-d.ts
Normal file
91
types/index.test-d.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import express, { RequestHandler } from 'express';
|
||||
import { expectType } from 'tsd'
|
||||
import * as promClient from 'prom-client';
|
||||
import promBundle, {
|
||||
type Middleware
|
||||
} from '..';
|
||||
|
||||
const middleware: express.RequestHandler = promBundle({ includeMethod: true });
|
||||
|
||||
expectType<string>(middleware.name);
|
||||
|
||||
promClient.register.clear();
|
||||
|
||||
expectType<Middleware>(promBundle({
|
||||
normalizePath: [
|
||||
// collect paths like "/customer/johnbobson" as just one "/custom/#name"
|
||||
['^/customer/.*', '/customer/#name'],
|
||||
|
||||
// collect paths like "/bobjohnson/order-list" as just one "/#name/order-list"
|
||||
['^.*/order-list', '/#name/order-list']
|
||||
],
|
||||
urlValueParser: {
|
||||
minHexLength: 5,
|
||||
extraMasks: [
|
||||
'ORD[0-9]{5,}' // replace strings like ORD1243423, ORD673562 as #val
|
||||
]
|
||||
}
|
||||
}));
|
||||
|
||||
promClient.register.clear();
|
||||
|
||||
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: 'histogram',
|
||||
metricsPath: '/prometheus',
|
||||
promClient: {
|
||||
collectDefaultMetrics: {
|
||||
}
|
||||
},
|
||||
promRegistry: new promClient.Registry(),
|
||||
urlValueParser: {
|
||||
minHexLength: 5,
|
||||
extraMasks: [
|
||||
'^[0-9]+\\.[0-9]+\\.[0-9]+$' // replace dot-separated dates with #val
|
||||
]
|
||||
},
|
||||
normalizePath: [
|
||||
['^/foo', '/example'] // replace /foo with /example
|
||||
],
|
||||
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: 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: express.Response) => res.statusCode.toString();
|
||||
|
||||
expectType<RequestHandler>(promBundle.clusterMetrics());
|
||||
|
||||
Reference in New Issue
Block a user