mirror of
https://github.com/BreizhHardware/express-prom-bundle.git
synced 2026-01-19 00:37:36 +01:00
Compare commits
231 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dcc9e1f893 | ||
|
|
36d1484d33 | ||
|
|
8fc61cd1d8 | ||
|
|
4fd7a797c5 | ||
|
|
30d9c3d473 | ||
|
|
10fe1cac8f | ||
|
|
9b4e0eb163 | ||
|
|
475b7a8f6d | ||
|
|
c1482220c2 | ||
|
|
66643411b8 | ||
|
|
97b2f425e6 | ||
|
|
a8d4fa8116 | ||
|
|
0eb0178622 | ||
|
|
5c1f68a482 | ||
|
|
12ff62478f | ||
|
|
2efc5189d0 | ||
|
|
c28b736c6e | ||
|
|
e84b96bf5e | ||
|
|
e49f95ab0e | ||
|
|
cf862fca31 | ||
|
|
0f6eab8d98 | ||
|
|
b586bd9515 | ||
|
|
1a5f6df7f9 | ||
|
|
b2f5283303 | ||
|
|
cdc558a8d8 | ||
|
|
af8a3097fc | ||
|
|
c5cce9f8f5 | ||
|
|
36789b54ae | ||
|
|
e1b09bca43 | ||
|
|
c3284c6ce6 | ||
|
|
8dc2f9c171 | ||
|
|
a9eb496d11 | ||
|
|
3353686379 | ||
|
|
489ce17366 | ||
|
|
4e07a7db7a | ||
|
|
59f6ac0afa | ||
|
|
597fdda556 | ||
|
|
21e90237e4 | ||
|
|
2de73f7526 | ||
|
|
e886d7cb05 | ||
|
|
815ba9819a | ||
|
|
f7805d301b | ||
|
|
6b23a109b1 | ||
|
|
39788a1ff0 | ||
|
|
f7474e3ea7 | ||
|
|
6da4bece87 | ||
|
|
522e9ad64d | ||
|
|
db8710d5d0 | ||
|
|
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 |
12
.devcontainer/devcontainer.json
Normal file
12
.devcontainer/devcontainer.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "Node.js 24",
|
||||
"image": "mcr.microsoft.com/devcontainers/base:ubuntu",
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/node:1": {
|
||||
"nodeGypDependencies": true,
|
||||
"version": "lts",
|
||||
"nvmVersion": "latest"
|
||||
},
|
||||
"ghcr.io/devcontainers/features/git-lfs:1": {}
|
||||
}
|
||||
}
|
||||
54
.eslintrc
54
.eslintrc
@@ -1,54 +0,0 @@
|
||||
{
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 6,
|
||||
"sourceType": "module"
|
||||
},
|
||||
|
||||
"globals": {
|
||||
"app": true,
|
||||
"fetch": true
|
||||
},
|
||||
|
||||
"env": {
|
||||
"node": true,
|
||||
"es6": true
|
||||
},
|
||||
|
||||
"extends": "eslint:recommended",
|
||||
|
||||
"rules": {
|
||||
"array-bracket-spacing": [2, "never"],
|
||||
"block-scoped-var": 2,
|
||||
"brace-style": [2, "1tbs"],
|
||||
"computed-property-spacing": [2, "never"],
|
||||
"curly": 2,
|
||||
"eol-last": 2,
|
||||
"eqeqeq": [2, "smart"],
|
||||
"max-depth": [1, 3],
|
||||
"new-cap": 1,
|
||||
"no-extend-native": 2,
|
||||
"no-mixed-spaces-and-tabs": 2,
|
||||
"no-trailing-spaces": 1,
|
||||
"no-unused-vars": 1,
|
||||
"no-use-before-define": [2, "nofunc"],
|
||||
"object-curly-spacing": [2, "never"],
|
||||
"quotes": [1, "single", "avoid-escape"],
|
||||
"semi": [2, "always"],
|
||||
"keyword-spacing": [2, {"before": true, "after": true}],
|
||||
"space-unary-ops": 2,
|
||||
"no-console": [1, { allow: ["info", "warn", "error"] }],
|
||||
|
||||
"max-len": [1, 120],
|
||||
"max-statements": [1, 50],
|
||||
|
||||
"consistent-this": [2, "self"],
|
||||
"no-var": 2,
|
||||
"no-dupe-class-members": 2,
|
||||
"operator-linebreak": [1, "before"],
|
||||
"no-unneeded-ternary": [1, {"defaultAssignment": false}],
|
||||
"no-lonely-if": 1,
|
||||
"linebreak-style": [2, "unix"],
|
||||
"no-nested-ternary": 2,
|
||||
"require-yield": 2
|
||||
}
|
||||
}
|
||||
12
.github/dependabot.yml
vendored
Normal file
12
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
target-branch: "dev"
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/.github/workflows"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
target-branch: "dev"
|
||||
42
.github/workflows/audit.yml
vendored
Normal file
42
.github/workflows/audit.yml
vendored
Normal file
@@ -0,0 +1,42 @@
|
||||
name: Security Audit
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, dev]
|
||||
pull_request:
|
||||
branches:
|
||||
- '**'
|
||||
schedule:
|
||||
- cron: '0 8 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
audit:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm install
|
||||
|
||||
- name: Run security audit
|
||||
id: audit
|
||||
run: npm audit --audit-level moderate
|
||||
continue-on-error: true
|
||||
|
||||
- name: Create issue on failure
|
||||
if: steps.audit.outcome == 'failure'
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
script: |
|
||||
github.rest.issues.create({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
title: 'Security Audit Failed',
|
||||
body: 'The daily security audit has failed. Please check the workflow run for details: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}',
|
||||
labels: ['security', 'audit']
|
||||
});
|
||||
72
.github/workflows/release.yml
vendored
Normal file
72
.github/workflows/release.yml
vendored
Normal file
@@ -0,0 +1,72 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [18, 20, 22, 24, latest]
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- run: npm install prom-client
|
||||
- run: npm install
|
||||
- run: npm run lint
|
||||
- run: npm test
|
||||
- run: npm run test-types
|
||||
- name: Generate coverage
|
||||
run: make coverage
|
||||
- name: Upload coverage to Coveralls
|
||||
uses: coverallsapp/github-action@v2
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
file: coverage/lcov.info
|
||||
|
||||
publish:
|
||||
needs: test
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- name: Use Node.js 18
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 18
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
- run: npm install
|
||||
- run: npm publish --access public
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
release:
|
||||
needs: publish
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Get version
|
||||
id: get_version
|
||||
run: echo "version=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT
|
||||
- name: Generate Changelog
|
||||
run: |
|
||||
# Simple changelog generation
|
||||
echo "# Changelog" > CHANGELOG.md
|
||||
git log --oneline --pretty=format:"- %s" $(git describe --tags --abbrev=0 2>/dev/null || echo "HEAD~1")..HEAD >> CHANGELOG.md
|
||||
- name: Create Release
|
||||
uses: actions/create-release@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
tag_name: v${{ steps.get_version.outputs.version }}
|
||||
release_name: Release v${{ steps.get_version.outputs.version }}
|
||||
body_path: CHANGELOG.md
|
||||
draft: false
|
||||
prerelease: false
|
||||
54
.github/workflows/test.yml
vendored
Normal file
54
.github/workflows/test.yml
vendored
Normal file
@@ -0,0 +1,54 @@
|
||||
name: CI-Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- dev
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- dev
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [18, 20, 22, 24, latest]
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- run: npm install prom-client
|
||||
- run: npm install
|
||||
- run: npm run lint
|
||||
- run: npm test
|
||||
- run: npm run test-types
|
||||
|
||||
codeql:
|
||||
name: CodeQL Analysis
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v4
|
||||
with:
|
||||
languages: javascript
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v4
|
||||
|
||||
audit:
|
||||
name: Audit Dependencies
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- name: Use Node.js 18
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 18
|
||||
- run: npm install
|
||||
- name: Audit Dependencies
|
||||
run: npm audit --audit-level=moderate
|
||||
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
|
||||
11
.travis.yml
11
.travis.yml
@@ -1,4 +1,11 @@
|
||||
language: node_js
|
||||
node_js:
|
||||
- "6"
|
||||
- "8"
|
||||
- "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/*' \
|
||||
|
||||
118
README.md
118
README.md
@@ -1,26 +1,29 @@
|
||||
[](https://travis-ci.org/jochen-schweizer/express-prom-bundle) [](https://coveralls.io/github/jochen-schweizer/express-prom-bundle?branch=master) [](https://www.tldrlegal.com/l/mit) [](http://badge.fury.io/js/express-prom-bundle)
|
||||
[](https://github.com/BreizhHardware/express-prom-bundle/actions/workflows/release.yml) [](https://coveralls.io/github/BreizhHardware/express-prom-bundle?branch=main) [](https://www.tldrlegal.com/l/mit) [](http://badge.fury.io/js/%40breizhhardware%2Fexpress-prom-bundle)
|
||||
|
||||
# express prometheus bundle
|
||||
|
||||
Express middleware with popular prometheus metrics in one bundle. It's also compatible with koa v1 and v2 (see below).
|
||||
Express middleware with popular prometheus metrics in one bundle. It's also compatible with koa v1 and v2 (see below). This package is a fork from the one from
|
||||
[Jochen Schweizer](https://github.com/jochen-schweizer/express-prom-bundle).
|
||||
|
||||
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`
|
||||
* `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 @breizhhardware/express-prom-bundle
|
||||
```
|
||||
|
||||
## Sample Usage
|
||||
|
||||
```javascript
|
||||
const promBundle = require("express-prom-bundle");
|
||||
const promBundle = require("@breizhhardware/express-prom-bundle");
|
||||
const app = require("express")();
|
||||
const metricsMiddleware = promBundle({includeMethod: true});
|
||||
|
||||
@@ -46,11 +49,31 @@ 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
|
||||
* **httpDurationMetricName**: Allows you change the name of HTTP duration metric, default: **`http_request_duration_seconds`**.
|
||||
* **upMetricName**: Allows you change the name of up metric, default: **`up`**.
|
||||
|
||||
Extra transformation callbacks:
|
||||
### 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`
|
||||
@@ -61,14 +84,29 @@ Extra transformation callbacks:
|
||||
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:
|
||||
### Other options ###
|
||||
|
||||
* **buckets**: buckets used for `http_request_duration_seconds` histogram
|
||||
* **autoregister**: if `/metrics` endpoint should be registered. (Default: **true**)
|
||||
* **autoregister**: if `/metrics` endpoint should be registered (default: **true**)
|
||||
* **promClient**: options for promClient startup, e.g. **collectDefaultMetrics**. This option was added
|
||||
to keep `express-prom-bundle` runnable using confit (e.g. with kraken.js) without writing any JS code,
|
||||
see [advanced example](https://github.com/jochen-schweizer/express-prom-bundle/blob/master/advanced-example.js)
|
||||
* **promRegistry**: Optional `promClient.Registry` instance to attach metrics to. Defaults to global `promClient.register`.
|
||||
* **metricsApp**: Allows you to attach the metrics endpoint to a different express app. You probably want to use it in combination with `autoregister: false`.
|
||||
* **bypass**: An object that takes onRequest and onFinish callbacks that determines whether the given request should be excluded in the metrics. Default:
|
||||
|
||||
```js
|
||||
{
|
||||
onRequest: (req) => false,
|
||||
onFinish: (req, res) => false
|
||||
}
|
||||
```
|
||||
|
||||
`onRequest` is run directly in the middleware chain, before the request is processed. `onFinish` is run after the request has been processed, and has access to the express response object in addition to the request object. Both callbacks are optional, and if one or both returns true the request is excluded.
|
||||
|
||||
As a shorthand, just the onRequest callback can be used instead of the object.
|
||||
|
||||
|
||||
### More details on includePath option
|
||||
|
||||
@@ -126,6 +164,17 @@ For more details:
|
||||
* [normalizePath.js](https://github.com/jochen-schweizer/express-prom-bundle/blob/master/src/normalizePath.js) - source code for path processing
|
||||
|
||||
|
||||
#### Example 3 (return express route definition):
|
||||
|
||||
```javascript
|
||||
app.use(promBundle(/* options? */));
|
||||
|
||||
promBundle.normalizePath = (req, opts) => {
|
||||
// Return the path of the express route (i.e. /v1/user/:id or /v1/timer/automated/:userid/:timerid")
|
||||
return req.route?.path ?? "NULL";
|
||||
};
|
||||
```
|
||||
|
||||
## express example
|
||||
|
||||
setup std. metrics but exclude `up`-metric:
|
||||
@@ -133,7 +182,7 @@ setup std. metrics but exclude `up`-metric:
|
||||
```javascript
|
||||
const express = require("express");
|
||||
const app = express();
|
||||
const promBundle = require("express-prom-bundle");
|
||||
const promBundle = require("@breizhhardware/express-prom-bundle");
|
||||
|
||||
// calls to this route will not appear in metrics
|
||||
// because it's applied before promBundle
|
||||
@@ -158,7 +207,7 @@ See an [advanced example on github](https://github.com/jochen-schweizer/express-
|
||||
## koa v2 example
|
||||
|
||||
```javascript
|
||||
const promBundle = require("express-prom-bundle");
|
||||
const promBundle = require("@breizhhardware/express-prom-bundle");
|
||||
const Koa = require("koa");
|
||||
const c2k = require("koa-connect");
|
||||
const metricsMiddleware = promBundle({/* options */ });
|
||||
@@ -170,11 +219,49 @@ 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('@breizhhardware/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.
|
||||
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 emails in the path as #val.
|
||||
while replacing all HEX values starting from 5 characters and all IP addresses in the path as #val.
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -191,13 +278,12 @@ while replacing all HEX values starting from 5 characters and all emails in the
|
||||
"buckets": [0.1, 1, 5],
|
||||
"promClient": {
|
||||
"collectDefaultMetrics": {
|
||||
"timeout": 2000
|
||||
}
|
||||
},
|
||||
"urlValueParser": {
|
||||
"minHexLength": 5,
|
||||
"extraMasks": [
|
||||
"^[0-9]+\\.[0-9]+\\.[0-9]+$"
|
||||
"^[0-9]+\\.[0-9]+\\.[0-9]+\\.[0-9]+$"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
12
SECURITY.md
Normal file
12
SECURITY.md
Normal file
@@ -0,0 +1,12 @@
|
||||
# Security Policy
|
||||
|
||||
## Supported Versions
|
||||
|
||||
| Version | Supported |
|
||||
| ------- | ------------------ |
|
||||
| >= 8.0.x| :white_check_mark: |
|
||||
| < 8.0.0 | :x: |
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
Please use [GitHub's private vulnerability reporting](https://github.com/breizhhardware/express-prom-bundle/security/advisories/new) to report a vulnerability.
|
||||
@@ -2,6 +2,9 @@
|
||||
|
||||
const express = require('express');
|
||||
const app = express();
|
||||
const promClient = require('prom-client');
|
||||
|
||||
// replace this with require('.') when running from library code
|
||||
const promBundle = require('express-prom-bundle');
|
||||
|
||||
const bundle = promBundle({
|
||||
@@ -10,15 +13,16 @@ const bundle = promBundle({
|
||||
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
|
||||
"^[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: [
|
||||
@@ -29,7 +33,7 @@ const bundle = promBundle({
|
||||
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) => {
|
||||
@@ -45,11 +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 localhost:3000/foo/09.08.2018\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
|
||||
`
|
||||
));
|
||||
|
||||
77
eslint.config.mjs
Normal file
77
eslint.config.mjs
Normal file
@@ -0,0 +1,77 @@
|
||||
import { defineConfig } from "eslint/config";
|
||||
import globals from "globals";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import js from "@eslint/js";
|
||||
import { FlatCompat } from "@eslint/eslintrc";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const compat = new FlatCompat({
|
||||
baseDirectory: __dirname,
|
||||
recommendedConfig: js.configs.recommended,
|
||||
allConfig: js.configs.all
|
||||
});
|
||||
|
||||
export default defineConfig([{
|
||||
extends: compat.extends("eslint:recommended"),
|
||||
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.node,
|
||||
app: true,
|
||||
fetch: true,
|
||||
},
|
||||
|
||||
ecmaVersion: 6,
|
||||
sourceType: "module",
|
||||
},
|
||||
|
||||
rules: {
|
||||
indent: [1, 2],
|
||||
"array-bracket-spacing": [2, "never"],
|
||||
"block-scoped-var": 2,
|
||||
"brace-style": [2, "1tbs"],
|
||||
"computed-property-spacing": [2, "never"],
|
||||
curly: 2,
|
||||
"eol-last": 2,
|
||||
eqeqeq: [2, "smart"],
|
||||
"max-depth": [1, 3],
|
||||
"new-cap": 1,
|
||||
"no-extend-native": 2,
|
||||
"no-mixed-spaces-and-tabs": 2,
|
||||
"no-trailing-spaces": 1,
|
||||
"no-unused-vars": 1,
|
||||
"no-use-before-define": [2, "nofunc"],
|
||||
"object-curly-spacing": [2, "never"],
|
||||
quotes: [1, "single", "avoid-escape"],
|
||||
semi: [2, "always"],
|
||||
|
||||
"keyword-spacing": [2, {
|
||||
before: true,
|
||||
after: true,
|
||||
}],
|
||||
|
||||
"space-unary-ops": 2,
|
||||
|
||||
"no-console": [1, {
|
||||
allow: ["info", "warn", "error"],
|
||||
}],
|
||||
|
||||
"max-len": [1, 120],
|
||||
"max-statements": [1, 50],
|
||||
"consistent-this": [2, "self"],
|
||||
"no-var": 2,
|
||||
"no-dupe-class-members": 2,
|
||||
"operator-linebreak": [1, "before"],
|
||||
|
||||
"no-unneeded-ternary": [1, {
|
||||
defaultAssignment: false,
|
||||
}],
|
||||
|
||||
"no-lonely-if": 1,
|
||||
"linebreak-style": [2, "unix"],
|
||||
"no-nested-ternary": 2,
|
||||
"require-yield": 2,
|
||||
},
|
||||
}]);
|
||||
7327
package-lock.json
generated
7327
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
49
package.json
49
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "express-prom-bundle",
|
||||
"version": "4.1.0",
|
||||
"name": "@breizhhardware/express-prom-bundle",
|
||||
"version": "8.0.7",
|
||||
"description": "express middleware with popular prometheus metrics in one bundle",
|
||||
"main": "src/index.js",
|
||||
"keywords": [
|
||||
@@ -10,32 +10,49 @@
|
||||
"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>",
|
||||
"author": "BreizhHardware <felix.marquet@horoquartz.com>",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/express": "^5.0.0",
|
||||
"on-finished": "^2.3.0",
|
||||
"prom-client": "~11.1.1",
|
||||
"url-value-parser": "^2.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"coveralls": "^3.0.2",
|
||||
"eslint": "^5.3.0",
|
||||
"express": "^4.16.3",
|
||||
"istanbul": "^0.4.5",
|
||||
"jasme": "^5.2.0",
|
||||
"koa": "^2.5.2",
|
||||
"koa-connect": "^2.0.0",
|
||||
"supertest": "^3.0.0",
|
||||
"supertest-koa-agent": "^0.3.0"
|
||||
"@eslint/eslintrc": "^3.3.1",
|
||||
"@eslint/js": "^9.38.0",
|
||||
"dts": "^0.1.1",
|
||||
"eslint": "^9.38.0",
|
||||
"express": "^5.1.0",
|
||||
"globals": "^16.4.0",
|
||||
"jasme": "^6.0.0",
|
||||
"koa": "^3.0.1",
|
||||
"koa-connect": "^2.0.1",
|
||||
"prom-client": "^15.0.0",
|
||||
"supertest": "^7.1.4",
|
||||
"supertest-koa-agent": "^0.3.0",
|
||||
"tsd": "^0.33.0",
|
||||
"typescript": "^5.9.3",
|
||||
"nyc": "^17.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"prom-client": ">=15.0.0"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/jochen-schweizer/express-prom-bundle.git"
|
||||
"url": "git+https://github.com/breizhhardware/express-prom-bundle.git"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
"node": ">=18"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,21 @@ 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,6 +49,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 +124,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 +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);
|
||||
@@ -122,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')
|
||||
@@ -139,26 +188,109 @@ 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();
|
||||
});
|
||||
@@ -185,9 +317,36 @@ describe('index', () => {
|
||||
});
|
||||
});
|
||||
|
||||
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 gloablly', done => {
|
||||
it('normalizePath can be replaced globally', done => {
|
||||
const app = express();
|
||||
const original = bundle.normalizePath;
|
||||
bundle.normalizePath = () => 'dummy';
|
||||
@@ -211,6 +370,40 @@ describe('index', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('respects pruneAgedBuckets', done => {
|
||||
const app = express();
|
||||
const metricsApp = express();
|
||||
const bundled = bundle({
|
||||
metricsApp,
|
||||
metricType: 'summary',
|
||||
includePath: true,
|
||||
maxAgeSeconds: 1,
|
||||
percentiles: [0.5],
|
||||
ageBuckets: 1,
|
||||
pruneAgedBuckets: true,
|
||||
});
|
||||
|
||||
app.use(bundled);
|
||||
|
||||
const agent = supertest(app);
|
||||
const metricsAgent = supertest(metricsApp);
|
||||
agent.get('/foo')
|
||||
.then(() => metricsAgent.get('/metrics'))
|
||||
.then(response => {
|
||||
expect(response.status).toBe(200);
|
||||
// up + bucket, sum, count
|
||||
expect(getMetricCount(response.text)).toBe(4);
|
||||
})
|
||||
.then(() => new Promise(r => setTimeout(r, 1010)))
|
||||
.then(() => metricsAgent.get('/metrics'))
|
||||
.then(response => {
|
||||
expect(response.status).toBe(200);
|
||||
// only up
|
||||
expect(getMetricCount(response.text)).toBe(1);
|
||||
})
|
||||
.finally(done);
|
||||
});
|
||||
|
||||
it('normalizePath function can be overridden', done => {
|
||||
const app = express();
|
||||
const instance = bundle({
|
||||
@@ -301,6 +494,27 @@ describe('index', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('handles errors in collectors', done => {
|
||||
const app = express();
|
||||
const instance = bundle({});
|
||||
app.use(instance);
|
||||
|
||||
new promClient.Gauge({
|
||||
name: 'kaboom',
|
||||
help: 'this metric explodes',
|
||||
collect() {
|
||||
throw new Error('kaboom!');
|
||||
}
|
||||
});
|
||||
|
||||
// the error will NOT be displayed if NODE_ENV=test (as defined in package.json)
|
||||
|
||||
supertest(app)
|
||||
.get('/metrics')
|
||||
.expect(500)
|
||||
.end((err) => done(err));
|
||||
});
|
||||
|
||||
it('customLabels={foo: "bar"} adds foo="bar" label to metrics', done => {
|
||||
const app = express();
|
||||
const instance = bundle({
|
||||
@@ -351,9 +565,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) {
|
||||
@@ -372,15 +584,136 @@ describe('index', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('calls promClient.collectDefaultMetrics', () => {
|
||||
const spy = spyOn(promClient, 'collectDefaultMetrics');
|
||||
bundle({
|
||||
promClient: {
|
||||
collectDefaultMetrics: {
|
||||
timeout: 3000
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
expect(spy).toHaveBeenCalledWith({timeout: 3000});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -28,4 +28,9 @@ describe('normalizePath', () => {
|
||||
});
|
||||
expect(subject).toThrow();
|
||||
});
|
||||
|
||||
it('uses urlPathReplacement when passed to transform the path', () => {
|
||||
expect(normalizePath({url: '/a/12345'}, {urlPathReplacement: ':id'}))
|
||||
.toBe('/a/:id');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,7 +6,11 @@ const normalizeStatusCode = require('../src/normalizeStatusCode');
|
||||
describe('normalizeStatusCode', () => {
|
||||
it('returns run callback if configured', () => {
|
||||
expect(
|
||||
normalizeStatusCode({status_code: 500})
|
||||
normalizeStatusCode({status_code: 500, headersSent: true})
|
||||
).toBe(500);
|
||||
});
|
||||
|
||||
it('returns 499 if headers are not sent', () => {
|
||||
expect(normalizeStatusCode({statusCode: 200, headersSent: false})).toBe(499);
|
||||
});
|
||||
});
|
||||
|
||||
260
src/index.js
260
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,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,29 @@ 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'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -72,97 +91,156 @@ function main(opts) {
|
||||
}
|
||||
|
||||
const httpMetricName = opts.httpDurationMetricName || 'http_request_duration_seconds';
|
||||
const upMetricName = opts.upMetricName || 'up';
|
||||
|
||||
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],
|
||||
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]
|
||||
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) {
|
||||
let prefix = '';
|
||||
if (opts.promClient && opts.promClient.collectDefaultMetrics) {
|
||||
prefix = opts.promClient.collectDefaultMetrics.prefix || '';
|
||||
}
|
||||
metrics.up = new promClient.Gauge({
|
||||
name: prefix + upMetricName,
|
||||
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 metricsMatch = opts.metricsPath instanceof RegExp ? opts.metricsPath
|
||||
: new RegExp('^' + (opts.metricsPath || '/metrics') + '/?$');
|
||||
|
||||
if (typeof opts.bypass === 'function') {
|
||||
opts.bypass = {
|
||||
onRequest: opts.bypass
|
||||
};
|
||||
} else if (!opts.bypass) {
|
||||
opts.bypass = {};
|
||||
}
|
||||
|
||||
const middleware = function (req, res, next) {
|
||||
const path = req.originalUrl || req.url; // originalUrl gets lost in koa-connect?
|
||||
let labels;
|
||||
|
||||
if (opts.autoregister && path.match(/^\/metrics\/?$/)) {
|
||||
return metricsMiddleware(req, res);
|
||||
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[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 = typeof opts.normalizePath == '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();
|
||||
});
|
||||
}
|
||||
const labels = {};
|
||||
const timer = metrics[httpMetricName].startTimer(labels);
|
||||
|
||||
onFinished(res, () => {
|
||||
if (opts.bypass.onFinish && opts.bypass.onFinish(req, res)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (opts.includeStatusCode) {
|
||||
labels.status_code = opts.formatStatusCode(res, opts);
|
||||
}
|
||||
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;
|
||||
|
||||
@@ -11,6 +11,7 @@ module.exports = function(req, opts) {
|
||||
// by middlewares such as 'router'. Note: this function is called onFinish
|
||||
/// i.e. always in the tail of the middleware chain
|
||||
let path = url.parse(req.originalUrl || req.url).pathname;
|
||||
const urlPathReplacement = opts ? opts.urlPathReplacement : '#val';
|
||||
|
||||
const normalizePath = opts && opts.normalizePath;
|
||||
if (Array.isArray(normalizePath)) {
|
||||
@@ -26,5 +27,5 @@ module.exports = function(req, opts) {
|
||||
if (!urlValueParser) {
|
||||
urlValueParser = new UrlValueParser(opts && opts.urlValueParser);
|
||||
}
|
||||
return urlValueParser.replacePathValues(path);
|
||||
return urlValueParser.replacePathValues(path, urlPathReplacement);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
'use strict';
|
||||
|
||||
const CLIENT_CLOSED_REQUEST_CODE = 499;
|
||||
|
||||
module.exports = function(res) {
|
||||
return res.status_code || res.statusCode;
|
||||
if (res.headersSent) {
|
||||
return res.status_code || res.statusCode;
|
||||
} else {
|
||||
return CLIENT_CLOSED_REQUEST_CODE;
|
||||
}
|
||||
};
|
||||
|
||||
91
types/index.d.ts
vendored
Normal file
91
types/index.d.ts
vendored
Normal file
@@ -0,0 +1,91 @@
|
||||
// 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;
|
||||
upMetricName?: 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