Compare commits

...

119 Commits
3.3.0 ... 6.3.2

Author SHA1 Message Date
Konstantin Pogorelov
2478e617bb bump 6.3.2 2021-02-18 15:31:34 +01:00
Konstantin Pogorelov
92db62dc90 run dtslint on travis with --onlyTestTsNext as a workaround for failing prom-client typing with older TS versions 2021-02-18 15:05:08 +01:00
Konstantin Pogorelov
3ffdeef8ae add linting (eslint) to travis 2021-02-18 13:04:39 +01:00
Konstantin Pogorelov
7c35f1beb6 add indentation rule for eslint 2021-02-18 13:03:25 +01:00
Konstantin Pogorelov
c44d157cfe codestyle: fix indentation 2021-02-18 13:01:26 +01:00
Konstantin Pogorelov
731fd3ec01 replace node_js v13 with v14 in travis config 2021-02-18 12:52:00 +01:00
Konstantin Pogorelov
3f587fb760 reduce minimal peer prom-client version to 12, allow any higher major version (let's be optimistic), see #76 2021-02-18 12:12:06 +01:00
Konstantin Pogorelov
3c2779d0d1 bump 6.3.1 2020-12-24 12:23:39 +01:00
Konstantin Pogorelov
a3c15b1645 support Promise response from .metrics() and .clusterMetrics(), see #71 2020-12-24 12:23:20 +01:00
Konstantin Pogorelov
f4677ce6c6 update dev. prom-client to v13, see #71 2020-12-24 12:17:02 +01:00
Konstantin Pogorelov
958453eb91 bump 6.3.0 2020-11-29 15:48:17 +01:00
Konstantin Pogorelov
0205d4cfc8 fix bypass test, minor change to docs, move bypass logic after serving the /metrics route, see MR #70 2020-11-29 15:47:28 +01:00
yacine
bffb4cf16e changed wording + added doc + unit test 2020-11-29 15:13:01 +01:00
yacine
52fdbf030f ability to exclude incoming request from metrics 2020-11-28 21:42:52 +01:00
Konstantin Pogorelov
c55d5f4862 fix duplicate httpDurationMetricName in types 2020-10-06 13:05:26 +02:00
Konstantin Pogorelov
75b1e2cafa bump 6.2.0 2020-10-06 12:54:37 +02:00
Konstantin Pogorelov
e2a8869b7f Merge pull request #43 from GabrielCastro/patch-1
update types to correct transformLabels
2020-10-06 12:51:26 +02:00
Konstantin Pogorelov
c64772dfc9 Merge pull request #55 from dpc/dpc-patch-1
Add a missing option to `Opts`
2020-10-06 12:49:33 +02:00
Konstantin Pogorelov
c1b1022fb5 Merge pull request #56 from Collaborne/pr/middleware-result
Declare that the metricsMiddleware is accessible on the bundle as well
2020-10-06 12:48:39 +02:00
Konstantin Pogorelov
16dfa7f926 bump version to 6.1.0 2020-07-22 18:24:43 +02:00
Konstantin Pogorelov
7e58bd3d06 Merge pull request #47 from jochen-schweizer/dependabot/npm_and_yarn/eslint-utils-1.4.3
Bump eslint-utils from 1.3.1 to 1.4.3
2020-07-22 18:19:46 +02:00
Konstantin Pogorelov
adf15f03f4 Merge pull request #52 from jochen-schweizer/dependabot/npm_and_yarn/acorn-6.4.1
Bump acorn from 6.0.4 to 6.4.1
2020-07-22 18:19:28 +02:00
Konstantin Pogorelov
6629c09dfa Merge pull request #60 from anxolin/add-missing-metric-name-opt
Add missing metric name parameter
2020-07-22 18:16:32 +02:00
Konstantin Pogorelov
fe2d1d25ec Merge pull request #64 from jochen-schweizer/dependabot/npm_and_yarn/handlebars-4.7.6
Bump handlebars from 4.0.12 to 4.7.6
2020-07-22 18:16:03 +02:00
Konstantin Pogorelov
a5d63925ce Merge pull request #62 from jonaskello/patch-1
Fix spelling
2020-07-22 18:15:30 +02:00
dependabot[bot]
43ba2091d3 Bump handlebars from 4.0.12 to 4.7.6
Bumps [handlebars](https://github.com/wycats/handlebars.js) from 4.0.12 to 4.7.6.
- [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.0.12...v4.7.6)

Signed-off-by: dependabot[bot] <support@github.com>
2020-07-22 16:13:09 +00:00
dependabot[bot]
1909018c17 Bump acorn from 6.0.4 to 6.4.1
Bumps [acorn](https://github.com/acornjs/acorn) from 6.0.4 to 6.4.1.
- [Release notes](https://github.com/acornjs/acorn/releases)
- [Commits](https://github.com/acornjs/acorn/compare/6.0.4...6.4.1)

Signed-off-by: dependabot[bot] <support@github.com>
2020-07-22 16:13:05 +00:00
dependabot[bot]
52f3a955df Bump eslint-utils from 1.3.1 to 1.4.3
Bumps [eslint-utils](https://github.com/mysticatea/eslint-utils) from 1.3.1 to 1.4.3.
- [Release notes](https://github.com/mysticatea/eslint-utils/releases)
- [Commits](https://github.com/mysticatea/eslint-utils/compare/v1.3.1...v1.4.3)

Signed-off-by: dependabot[bot] <support@github.com>
2020-07-22 16:12:59 +00:00
Konstantin Pogorelov
e2800e7d30 Merge pull request #53 from jochen-schweizer/dependabot/npm_and_yarn/js-yaml-3.13.1
Bump js-yaml from 3.12.0 to 3.13.1
2020-07-22 18:12:31 +02:00
Konstantin Pogorelov
fff0384605 Merge pull request #63 from jochen-schweizer/dependabot/npm_and_yarn/lodash-4.17.19
Bump lodash from 4.17.11 to 4.17.19
2020-07-22 18:12:11 +02:00
Konstantin Pogorelov
514745b23a Merge pull request #46 from aaronleesmith/feature/customizable-prom-registry
Allows for customizing promRegistry when creating prom bundle.
2020-07-22 18:10:08 +02:00
dependabot[bot]
77730b9df5 Bump lodash from 4.17.11 to 4.17.19
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.11 to 4.17.19.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.11...4.17.19)

Signed-off-by: dependabot[bot] <support@github.com>
2020-07-16 00:02:18 +00:00
Jonas Kello
6d9da12d97 Fix spelling 2020-06-06 18:59:44 +02:00
Anxo Rodriguez
f1f36f0fb7 Add missing metric name parameter 2020-04-27 19:36:43 +02:00
Andreas Kohn
d2f0088f3c Declare that the metricsMiddleware is accessible on the bundle as well
Related-to: #49
2020-04-09 21:09:38 +02:00
Dawid Ciężarkiewicz
4f89424c79 Add a missing option to Opts 2020-04-09 09:28:22 -07:00
dependabot[bot]
33765b3654 Bump js-yaml from 3.12.0 to 3.13.1
Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 3.12.0 to 3.13.1.
- [Release notes](https://github.com/nodeca/js-yaml/releases)
- [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodeca/js-yaml/compare/3.12.0...3.13.1)

Signed-off-by: dependabot[bot] <support@github.com>
2020-03-21 18:08:10 +00:00
Konstantin Pogorelov
1a8f38c983 Merge pull request #51 from sam-github/update-prom-client
Update to prom-client v12, requiring Node >=10
2020-03-21 19:07:39 +01:00
Konstantin Pogorelov
23cedb9f05 bump to 6.0.0 2020-03-21 18:49:56 +01:00
Sam Roberts
20c6a7de24 Update to prom-client v12, requiring Node >=10
v12 drops support for versions of Node.js that are no longer supported
(as-of January, 2020), and also removes the timeout option from
collectDefaultMetrics().

Remove 6 and 8 from Travis, and add 12 and 13 (so all the non-EOL versions
of Node.js are tested).
2020-03-06 16:39:47 -08:00
Aaron Smith
0a1fda31de Fixes lint issue. 2019-09-19 11:39:11 -06:00
Aaron Smith
7654a4ec79 Allows for customizing promRegistry when creating prom bundle. 2019-09-19 11:10:43 -06:00
Gabriel Castro
faeeb7b57b update types to correct transformLabels returning
`transformLabels` should return void as the library expects
the labels object to be modified in place and does not use the return value
2019-07-22 16:38:14 -04:00
Konstantin Pogorelov
a21bd439cf bump 5.1.5 2019-05-01 14:53:47 +02:00
Konstantin Pogorelov
90f2d90e46 add dtslint to the main lint target 2019-05-01 14:53:38 +02:00
Konstantin Pogorelov
71c0e52020 Merge pull request #38 from raszi/fix-types
fix(types): fix type definitions and add tests
2019-05-01 14:50:34 +02:00
KARASZI István
78e25d6587 Add back index.d.ts 2019-05-01 14:15:45 +02:00
KARASZI István
02f3fe9008 Simplify type definition 2019-04-30 22:54:46 +02:00
KARASZI István
2a5e25dd77 Fix line ending 2019-04-30 22:46:49 +02:00
KARASZI István
f770ab800d Try to fix Node 6 issue 2019-04-30 22:46:03 +02:00
KARASZI István
6041bee4dd Run dtslint on Travis 2019-04-30 22:40:33 +02:00
KARASZI István
a92b4cfbe6 Remove unnecessary newline 2019-04-30 22:36:23 +02:00
KARASZI István
8df2778337 Fix types and add tests 2019-04-30 22:26:39 +02:00
Konstantin Pogorelov
b00d88c4a7 bump 5.1.4 2019-04-16 13:13:12 +02:00
Konstantin Pogorelov
11988cb025 Merge pull request #37 from cerodriguezl/fix-ts-definition
Fix the definition of the Opts interface
2019-04-16 13:12:39 +02:00
Konstantin Pogorelov
df6854ee04 Merge pull request #36 from gabordobrei/master
make transformLabels optional in index.d.ts
2019-04-16 13:08:14 +02:00
Carlos Rodriguez
48d883b7f2 Fix the definition of the Opts interface
The `buckets` property was defined as a `Tuple` (`[number]`) instead of an Array
(`number[]`).
2019-04-15 18:21:35 -05:00
Gábor Döbrei
b835e90560 make transformLabels optional in index.d.ts 2019-04-15 15:53:11 +02:00
Konstantin Pogorelov
088ff5a063 make customLabels optional in index.d.ts 2019-04-09 22:15:36 +02:00
Konstantin Pogorelov
fd12de6ea3 bump 5.1.3 2019-04-09 22:10:00 +02:00
Konstantin Pogorelov
0b8706dd42 remove trailing spaces 2019-04-09 22:09:44 +02:00
Konstantin Pogorelov
982ff86543 Merge pull request #35 from aaronleesmith/master
add customLabels to index.d.ts
2019-04-09 22:05:51 +02:00
Aaron Smith
5f39d8f357 Update index.d.ts
Custom labels was added as an option but never added to the typescript definition file, breaking the usage during typescript compilation.
2019-04-09 13:54:17 -06:00
Konstantin Pogorelov
d405e3f584 bump 5.1.2 2019-04-08 12:02:45 +02:00
Konstantin Pogorelov
ff147d8fc4 Merge pull request #34 from cliedeman/fix-typedef
typedefs: normalizePath should return a string
2019-04-08 12:01:47 +02:00
Ciaran Liedeman
43f5a2b04f typedefs: normalizePath should return a string 2019-04-08 10:21:53 +02:00
Konstantin Pogorelov
09ee5d954a bump 5.1.1 2019-04-05 18:10:33 +02:00
Konstantin Pogorelov
33cca0d2cf remove .npmignore because files are now whitelisted 2019-04-05 18:09:56 +02:00
Konstantin Pogorelov
2040c043fd Merge branch 'cliedeman-feat/typescript-definitions' 2019-04-05 18:02:10 +02:00
Konstantin Pogorelov
f6466d007b Merge pull request #33 from cliedeman/feat/typescript-definitions
Added Typescript definitions
2019-04-05 18:01:33 +02:00
Konstantin Pogorelov
e68466d7af experimental workaround for testing cluster on travis 2019-04-05 17:56:22 +02:00
Ciaran Liedeman
6b49ffab08 Added Typescript definitions 2019-04-04 11:31:41 +02:00
Konstantin Pogorelov
cb128c7520 bump 5.1.0 2019-03-14 13:37:08 +01:00
Konstantin Pogorelov
d336165848 extract metricType as a seprate odc section 2019-03-14 13:36:54 +01:00
Konstantin Pogorelov
cfe0065146 minor wording changes 2019-03-14 13:17:47 +01:00
Konstantin Pogorelov
963a66a25e Merge pull request #30 from caraboides/master
pass maxAgeSeconds & ageBuckets to summary metrics
2019-03-14 13:06:33 +01:00
Christian Hennig
389684b426 Documentation for sliding window for summary metrics 2019-03-14 11:58:10 +01:00
Konstantin Pogorelov
846ccfc641 Update src/index.js
Co-Authored-By: caraboides <caraboides@googlemail.com>
2019-03-14 12:47:08 +02:00
Christian Hennig
17fe5f4c70 pass maxAgeSeconds & ageBuckets to summary metrics 2019-03-14 10:18:55 +01:00
Konstantin Pogorelov
66ef4e3176 bump 5.0.2 2018-12-24 12:50:44 +01:00
Konstantin Pogorelov
f55d397a31 disable notifications in travis-ci 2018-12-24 12:50:25 +01:00
Konstantin Pogorelov
68ad108f77 correct sample usage 2018-12-24 12:49:41 +01:00
Konstantin Pogorelov
6000056f7a bump 5.0.1 2018-12-24 02:26:14 +01:00
Konstantin Pogorelov
48856afbe3 add .vscode and Makefile to .npmignore 2018-12-24 02:18:48 +01:00
Konstantin Pogorelov
e035e2b991 add before_install to .travis.yml for peer dependency 2018-12-24 02:12:03 +01:00
Konstantin Pogorelov
c6d5964768 add metricPath and includeUp options, make whitelist/blacklist obsolete, simnplify code, update unit tests and code, add node10 to .travis.yml 2018-12-24 02:06:16 +01:00
Konstantin Pogorelov
94722e908b make prom-client a peer dependency, upgrade all packages, bump 5.0.0 2018-12-23 21:39:00 +01:00
Konstantin Pogorelov
00b8369329 Merge branch 'develop' 2018-12-23 18:48:10 +01:00
Konstantin Pogorelov
59221f891b bump 4.3.0 2018-12-23 18:41:02 +01:00
Konstantin Pogorelov
3a0b2caf61 upgrade prom-cliet to 11.2.1 (latest) 2018-12-23 18:40:28 +01:00
Konstantin Pogorelov
e7d004f0cc rename metricsType -> metricType, move corresponding readme block 2 paragraphs lower 2018-12-23 16:45:38 +01:00
Chen Li
0dd3116f23 Feature/add metrics type summary (#24)
* add metric type summary

* add tests for percentile option

* throw errors for unknown metricType

* set histogram as default metrics type
2018-12-23 16:36:47 +01:00
Konstantin Pogorelov
0606bf5f4f remove a duplicate cluster docs piece, versio 4.2.1 2018-08-26 14:00:25 +02:00
Konstantin Pogorelov
6054824c67 Merge branch 'master' into develop 2018-08-25 14:43:58 +02:00
Konstantin Pogorelov
ec9835270f version 4.2.0 2018-08-25 13:11:31 +02:00
Konstantin Pogorelov
01c78bcc1d Merge branch 'master' into develop 2018-08-25 13:10:39 +02:00
Konstantin Pogorelov
1cf8c86acb Squashed commit of the following:
commit bc65dc45cb
Author: Konstantin Pogorelov <or@pluseq.com>
Date:   Sat Aug 25 12:57:15 2018 +0200

    #16 fix typo in docs

commit 2003e7743f
Author: Konstantin Pogorelov <or@pluseq.com>
Date:   Sat Aug 25 12:44:24 2018 +0200

    #16 fix unit test (cluster_metrics/metrics_cluster), cover the error branch

commit 568c87216a
Author: Konstantin Pogorelov <or@pluseq.com>
Date:   Sat Aug 25 12:42:53 2018 +0200

    #16 remove unnecessary check for existance after new, break on error with 500

commit 98be36244e
Author: Konstantin Pogorelov <or@pluseq.com>
Date:   Sat Aug 25 12:40:15 2018 +0200

    #16 make the cluster example runnable
2018-08-25 13:07:36 +02:00
Konstantin Pogorelov
43b51d4ab1 fix jasme path (again) 2018-08-25 12:58:44 +02:00
Konstantin Pogorelov
f6e87b7697 revert way jasme was called, add npm run coverage 2018-08-25 12:58:34 +02:00
Konstantin Pogorelov
3be8d53a27 fix newlines in index.spec.js 2018-08-25 12:58:24 +02:00
Konstantin Pogorelov
34e6a21be1 update package-lock.json 2018-08-25 12:58:20 +02:00
Adam Yost
02fcda4721 Add support for clusterMaster option re: #16 (#17)
* Add support for clusterMaster option re: #16
* Add Cluster instructions to README
* Use the approach recommended in PR
* use console.error for errors
* Update with new method signature
* add code coverage for new clusterMetrics middleware
2018-08-25 12:58:09 +02:00
Konstantin Pogorelov
bc65dc45cb #16 fix typo in docs 2018-08-25 12:57:15 +02:00
Konstantin Pogorelov
8cae5b6ef3 fix jasme path (again) 2018-08-25 12:52:38 +02:00
Konstantin Pogorelov
2003e7743f #16 fix unit test (cluster_metrics/metrics_cluster), cover the error branch 2018-08-25 12:44:24 +02:00
Konstantin Pogorelov
568c87216a #16 remove unnecessary check for existance after new, break on error with 500 2018-08-25 12:42:53 +02:00
Konstantin Pogorelov
98be36244e #16 make the cluster example runnable 2018-08-25 12:40:15 +02:00
Konstantin Pogorelov
6ff1204db4 revert way jasme was called, add npm run coverage 2018-08-25 09:49:28 +02:00
Konstantin Pogorelov
f71d837660 fix newlines in index.spec.js 2018-08-25 09:45:09 +02:00
Konstantin Pogorelov
4aa2bfa6ae update package-lock.json 2018-08-24 23:48:22 +02:00
Adam Yost
1fff877787 Add support for clusterMaster option re: #16 (#17)
* Add support for clusterMaster option re: #16
* Add Cluster instructions to README
* Use the approach recommended in PR
* use console.error for errors
* Update with new method signature
* add code coverage for new clusterMetrics middleware
2018-08-24 23:36:23 +02:00
Konstantin Pogorelov
5e0cd75673 add normalizePath option as tuple array, improve docs and advanced example, version to 4.1.0 2018-08-09 11:27:04 +02:00
Konstantin Pogorelov
d292dcab33 remove changelog from readme as it duplicates github releases function 2018-08-08 12:00:29 +02:00
Konstantin Pogorelov
84f99cc49c add urlValueParser option, update docs accordingly 2018-08-08 11:23:27 +02:00
Konstantin Pogorelov
fffe35ce5e update dependencies, use url-value-parser v2 2018-08-08 11:22:03 +02:00
Konstantin Pogorelov
06341c227a wip: 4.0.0, prom-client to v11, url-value-parser to v2 2018-08-07 17:55:25 +02:00
Konstantin Pogorelov
45b8f373be include collectDefaultMetrics in kraken example 2018-01-26 17:40:05 +01:00
Konstantin Pogorelov
4ee269faee make test more specific 2018-01-23 16:58:49 +01:00
Konstantin Pogorelov
e4d6113ff2 replace node 7 with node 8 in .travis.yml 2018-01-23 16:52:12 +01:00
17 changed files with 2613 additions and 1630 deletions

View File

@@ -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
View File

@@ -1,3 +1,5 @@
.npmrc
node_modules
coverage
/.vscode

View File

@@ -1,6 +0,0 @@
advanced-example.js
docker-compose.yml
spec
.travis.yml
.eslintrc
coverage

View File

@@ -1,4 +1,13 @@
language: node_js
node_js:
- "6"
- "7"
- "10"
- "12"
- "14"
notifications:
email: false
before_install:
- npm install prom-client
script:
- npm run lint
- npm test
- npm run dtslint-next

View File

@@ -4,6 +4,7 @@ test:
./node_modules/jasme/run.js
lint:
node_modules/eslint/bin/eslint.js src
node_modules/.bin/dtslint types
coverage:
node_modules/istanbul/lib/cli.js cover \
-i 'src/*' \

177
README.md
View File

@@ -4,17 +4,17 @@
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
Since version 5 it uses **prom-client** as a peer dependency. See: https://github.com/siimon/prom-client
Included metrics:
* `up`: normally is just 1
* `http_request_duration_seconds`: http latency histogram labeled with `status_code`, `method` and `path`
* `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
```
## Sample Usage
@@ -46,29 +46,47 @@ 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 importent details below), 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
* **bypass**: function taking express request as an argument and determines whether the given request should be excluded in the metrics, default: **() => false**
Extra transformation callbacks:
### metricType option ###
* **normalizePath**: `function(req)` generates path values from express `req` (see details below)
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
### 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**
Other options:
### Other options ###
* **buckets**: buckets used for `http_request_duration_seconds` histogram
* **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)
Deprecated:
* **whitelist**, **blacklist**: array of strings or regexp specifying which metrics to include/exclude (there are only 2 metrics)
* **excludeRoutes**: 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 to use express or meddleware features instead of this option.
* **httpDurationMetricName**: name of the request duration histogram metric. (Default: `http_request_duration_seconds`)
* **promRegistry**: Optional `promClient.Registry` instance to attach metrics to. Defaults to global `promClient.register`.
### More details on includePath option
@@ -80,10 +98,33 @@ 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.
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? */));
@@ -93,8 +134,8 @@ app.use(promBundle(/* options? */));
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;
};
```
@@ -103,7 +144,6 @@ For more details:
* [normalizePath.js](https://github.com/jochen-schweizer/express-prom-bundle/blob/master/src/normalizePath.js) - source code for path processing
## express example
setup std. metrics but exclude `up`-metric:
@@ -148,9 +188,47 @@ 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('./src/index');
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 {
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
{
@@ -163,7 +241,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]+$"
]
}
}
]
}
@@ -172,50 +261,6 @@ Here is meddleware config sample, which can be used in a standard **kraken.js**
}
```
## Changelog
* **3.3.0**
* added option **promClient** to be able to call collectDefaultMetrics
* upgrade **prom-client** to ~10.2.2 (switch to semver "approximately")
* **3.2.0**
* added options **customLabels**, **transformLabels**
* upgrade **prom-client** to 10.1.0
* **3.1.0**
* upgrade **prom-client** to 10.0.0
* **3.0.0**
* upgrade dependencies, most notably **prom-client** to 9.0.0
* switch to koa v2 in koa unittest
* only node v6 or higher is supported (stop supporting node v4 and v5)
* switch to npm5 and use package-lock.json
* options added: includeStatusCode, formatStatusCode
* **2.1.0**
* deprecate **excludeRoutes**, use **req.originalUrl** 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

View File

@@ -2,26 +2,35 @@
const express = require('express');
const app = express();
const promClient = require('prom-client');
const promBundle = require('express-prom-bundle');
const bundle = promBundle({
blacklist: [/up/],
buckets: [0.1, 0.4, 0.7],
includeMethod: true,
includePath: true,
customLabels: {year: null},
transformLabels: labels => Object.assign(labels, {year: new Date().getFullYear()}),
metricsPath: '/prometheus',
promClient: {
collectDefaultMetrics: {
timeout: 1000
}
}
},
urlValueParser: {
minHexLength: 5,
extraMasks: [
"^[0-9]+\\.[0-9]+\\.[0-9]+$" // replace dot-separated dates with #val
]
},
normalizePath: [
['^/foo', '/example'] // replace /foo with /example
]
});
app.use(bundle);
// native prom-client metric (no prefix)
const c1 = new bundle.promClient.Counter({name: 'c1', help: 'c1 help'});
const c1 = new promClient.Counter({name: 'c1', help: 'c1 help'});
c1.inc(10);
app.get('/foo/:id', (req, res) => {
@@ -37,10 +46,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
`
));

3178
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": "3.3.0",
"version": "6.3.2",
"description": "express middleware with popular prometheus metrics in one bundle",
"main": "src/index.js",
"keywords": [
@@ -10,32 +10,47 @@
"path",
"method"
],
"files": [
"src",
"types/index.d.ts"
],
"types": "types",
"scripts": {
"test": "node_modules/jasme/run.js"
"test": "node_modules/jasme/run.js",
"lint": "eslint src",
"coverage": "make coverage",
"dtslint": "dtslint types",
"dtslint-next": "dtslint --onlyTestTsNext types"
},
"author": "Konstantin Pogorelov <or@pluseq.com>",
"license": "MIT",
"dependencies": {
"on-finished": "^2.3.0",
"prom-client": "~10.2.2",
"url-value-parser": "^1.0.0"
"url-value-parser": "^2.0.0"
},
"devDependencies": {
"coveralls": "^2.13.1",
"eslint": "^3.19.0",
"express": "^4.15.3",
"@types/express": "^4.16.1",
"coveralls": "^3.0.2",
"dtslint": "^0.7.1",
"eslint": "^5.11.0",
"express": "^4.16.4",
"istanbul": "^0.4.5",
"jasme": "^5.2.0",
"koa": "^2.2.0",
"koa-connect": "^2.0.0",
"supertest": "^3.0.0",
"supertest-koa-agent": "^0.3.0"
"jasme": "^6.0.0",
"koa": "^2.6.2",
"koa-connect": "^2.0.1",
"prom-client": "^13.0.0",
"supertest": "^3.3.0",
"supertest-koa-agent": "^0.3.0",
"typescript": "^3.4.5"
},
"peerDependencies": {
"prom-client": ">=12.0.0"
},
"repository": {
"type": "git",
"url": "https://github.com/jochen-schweizer/express-prom-bundle.git"
},
"engines": {
"node": ">=6.0.0"
"node": ">=10"
}
}

View File

@@ -8,6 +8,7 @@ const Koa = require('koa');
const c2k = require('koa-connect');
const supertestKoa = require('supertest-koa-agent');
const promClient = require('prom-client');
const cluster = require('cluster');
describe('index', () => {
beforeEach(() => {
@@ -17,7 +18,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,6 +35,64 @@ describe('index', () => {
});
});
it('"up"-metric can be excluded', done => {
const app = express();
const bundled = bundle({
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({
@@ -51,9 +110,7 @@ describe('index', () => {
it('metrics should be attached to /metrics by default', done => {
const app = express();
const bundled = bundle({
whitelist: ['up']
});
const bundled = bundle();
app.use(bundled);
const agent = supertest(app);
@@ -82,27 +139,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);
@@ -139,22 +175,67 @@ 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 metricHashMap = instance.metrics.http_request_duration_seconds.hashMap;
expect(metricHashMap['status_code:200']).toBeDefined();
// only /good-word should be counted
expect(metricHashMap['status_code:200'].count).toBe(1);
agent
.get('/metrics')
.end((err, res) => {
expect(res.status).toBe(200);
done();
});
});
});
});
});
@@ -185,50 +266,105 @@ describe('index', () => {
});
});
it('normalizePath can be replaced gloablly', done => {
it('metric type histogram works', done => {
const app = express();
const original = bundle.normalizePath;
bundle.normalizePath = () => 'dummy';
const instance = bundle({
includePath: true,
const bundled = bundle({
metricType: 'histogram',
buckets: [10, 100],
});
app.use(instance);
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(/"dummy"/m);
bundle.normalizePath = original;
done();
});
});
agent.get('/test').end(() => {
agent
.get('/metrics')
.end((err, res) => {
expect(res.status).toBe(200);
expect(res.text).toMatch(/le="100"/);
done();
});
});
});
it('normalizePath 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('throws on unknown metricType ', () => {
expect(() => {
bundle({metricType: 'hello'});
}).toThrow();
});
describe('usage of normalizePath()', () => {
it('normalizePath can be replaced gloablly', 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('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 => {
@@ -267,7 +403,7 @@ describe('index', () => {
.get('/metrics')
.end((err, res) => {
expect(res.status).toBe(200);
expect(res.text).not.toMatch(/200/);
expect(res.text).not.toMatch(/="200"/);
done();
});
});
@@ -323,9 +459,7 @@ describe('index', () => {
it('Koa: metrics returns up=1', done => {
const app = new Koa();
const bundled = bundle({
whitelist: ['up']
});
const bundled = bundle();
app.use(c2k(bundled));
app.use(function(ctx, next) {
@@ -349,10 +483,91 @@ describe('index', () => {
bundle({
promClient: {
collectDefaultMetrics: {
timeout: 3000
}
}
});
expect(spy).toHaveBeenCalledWith({timeout: 3000});
expect(spy).toHaveBeenCalledWith({});
});
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();
});
});
});
});
});

View File

@@ -8,4 +8,24 @@ describe('normalizePath', () => {
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();
});
});

View File

@@ -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,38 +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,
includeStatusCode: true,
normalizePath: main.normalizePath,
formatStatusCode: main.normalizeStatusCode,
promClient: {}
},
opts
);
if (arguments[2] && arguments[1] && arguments[1].send) {
arguments[1].status(500)
.send('<h1>500 Error</h1>\n'
@@ -58,12 +60,28 @@ 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
}, 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'
);
}
@@ -73,94 +91,126 @@ function main(opts) {
const httpMetricName = opts.httpDurationMetricName || 'http_request_duration_seconds';
const metricTemplates = {
'up': () => new promClient.Gauge({
name: 'up',
help: '1 = up, 0 = not up'
}),
[httpMetricName]: () => {
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));
}
const metric = new promClient.Histogram({
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]
});
} 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]
buckets: opts.buckets || [0.003, 0.03, 0.1, 0.3, 1.5, 10],
registers: [opts.promRegistry]
});
return metric;
} else {
throw new Error('metricType option must be histogram or summary');
}
};
const metrics = {};
const names = prepareMetricNames(opts, metricTemplates);
for (let name of names) {
metrics[name] = metricTemplates[name]();
}
if (metrics.up) {
const metrics = {
[httpMetricName]: makeHttpMetric()
};
if (opts.includeUp !== false) {
metrics.up = new promClient.Gauge({
name: 'up',
help: '1 = up, 0 = not up',
registers: [opts.promRegistry]
});
metrics.up.set(1);
}
const metricsMiddleware = function(req, res) {
const metricsMiddleware = function(req, res, next) {
res.writeHead(200, {'Content-Type': 'text/plain'});
res.end(promClient.register.metrics());
const metricsResponse = opts.promRegistry.metrics();
// starting from prom-client@13 .metrics() returns a Promise
if (metricsResponse.then) {
metricsResponse
.then(output => res.end(output))
.catch(err => next(err));
} else {
// compatibility fallback for previous versions of prom-client@<=12
res.end(metricsResponse);
}
};
const metricsMatch = opts.metricsPath instanceof RegExp ? opts.metricsPath
: new RegExp('^' + (opts.metricsPath || '/metrics') + '/?$');
const middleware = function (req, res, next) {
const path = req.originalUrl || req.url; // originalUrl gets lost in koa-connect?
let labels;
if (opts.autoregister && path.match(/^\/metrics\/?$/)) {
if (opts.autoregister && path.match(metricsMatch)) {
return metricsMiddleware(req, res);
}
// bypass() is checked only after /metrics was processed
// if you wish to disable /metrics use autoregister:false instead
if (opts.bypass && opts.bypass(req)) {
return next();
}
if (opts.excludeRoutes && matchVsRegExps(path, opts.excludeRoutes)) {
return next();
}
if (metrics[httpMetricName]) {
labels = {};
let timer = metrics[httpMetricName].startTimer(labels);
onFinished(res, () => {
if (opts.includeStatusCode) {
labels.status_code = opts.formatStatusCode(res, opts);
}
if (opts.includeMethod) {
labels.method = req.method;
}
if (opts.includePath) {
labels.path = opts.normalizePath(req, opts);
}
if (opts.customLabels) {
Object.assign(labels, opts.customLabels);
}
if (opts.transformLabels) {
opts.transformLabels(labels, req, res);
}
timer();
});
}
const labels = {};
const timer = metrics[httpMetricName].startTimer(labels);
onFinished(res, () => {
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;
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;

View File

@@ -2,16 +2,29 @@
const UrlValueParser = require('url-value-parser');
const url = require('url');
// ATTENTION! urlValueParser is a singleton!
let urlValueParser;
module.exports = function(req) {
module.exports = function(req, 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 || req.url).pathname;
let path = url.parse(req.originalUrl || req.url).pathname;
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);
};

66
types/index.d.ts vendored Normal file
View File

@@ -0,0 +1,66 @@
// TypeScript Version: 2.8
import { Request, RequestHandler, Response } from 'express';
import { DefaultMetricsCollectorConfiguration, Registry } 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 Opts {
autoregister?: boolean;
buckets?: number[];
customLabels?: { [key: string]: any };
includeStatusCode?: boolean;
includeMethod?: boolean;
includePath?: boolean;
includeUp?: boolean;
bypass?: (req: Request) => boolean;
metricType?: 'summary' | 'histogram';
metricsPath?: string;
httpDurationMetricName?: string;
promClient?: { collectDefaultMetrics?: DefaultMetricsCollectorConfiguration };
promRegistry?: Registry;
normalizePath?: NormalizePathEntry[] | NormalizePathFn;
formatStatusCode?: NormalizeStatusCodeFn;
transformLabels?: TransformLabelsFn;
// https://github.com/disjunction/url-value-parser#options
urlValueParser?: {
minHexLength?: number;
minBase64Length?: number;
replaceMasks?: string[];
extraMasks?: string[];
};
}
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;

79
types/test.ts Normal file
View File

@@ -0,0 +1,79 @@
import { Request, RequestHandler, Response } from 'express';
import * as promClient from 'prom-client';
import * as promBundle from 'express-prom-bundle';
// $ExpectType Middleware
const middleware: 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,
customLabels: { year: null },
transformLabels: (labels: promBundle.Labels) => ({
...labels,
year: new Date().getFullYear()
}),
metricType: 'summary',
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: Response) => res.statusCode + 100
});
// 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) => {
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();
// $ExpectType RequestHandler
promBundle.clusterMetrics();
// Missing test
// const stringReturn: string = promBundle.normalizePath({}, {});

14
types/tsconfig.json Normal file
View File

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

3
types/tslint.json Normal file
View File

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