Express middleware with popular prometheus metrics in one bundle. It's also compatible with koa v1 and v2 (see below).
Since version 5 it uses prom-client as a peer dependency. See: https://github.com/siimon/prom-client
Included metrics:
up
: normally is just 1http_request_duration_seconds
: http latency histogram/summary labeled withstatus_code
,method
andpath
npm install prom-client express-prom-bundle
const promBundle = require("express-prom-bundle");
const app = require("express")();
const metricsMiddleware = promBundle({includeMethod: true});
app.use(metricsMiddleware);
app.use(/* your middleware */);
app.listen(3000);
- call your endpoints
- see your metrics here: http://localhost:3000/metrics
ALERT!
The order in which the routes are registered is important, since only the routes registered after the express-prom-bundle will be measured
You can use this to your advantage to bypass some of the routes. See the example below.
Which labels to include in http_request_duration_seconds
metric:
- includeStatusCode: HTTP status code (200, 400, 404 etc.), default: true
- includeMethod: HTTP method (GET, PUT, ...), default: false
- includePath: URL path (see important details below), default: false
- customLabels: an object containing extra labels, e.g.
{project_name: 'hello_world'}
. Most useful together with transformLabels callback, otherwise it's better to use native Prometheus relabeling. - includeUp: include an auxiliary "up"-metric which always returns 1, default: true
- metricsPath: replace the
/metrics
route with a regex or exact string. Note: it is highly recommended to just stick to the default - metricType: histogram/summary selection. See more details below
- bypass: function taking express request as an argument and determines whether the given request should be excluded in the metrics, default: () => false
Two metric types are supported for http_request_duration_seconds
metric:
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
- normalizePath:
function(req)
orArray
- 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]
. Theregex
can be a string and is automatically converted into JS regex. - ... see more details in the section below
- if function is provided, then it should generate path value from express
- urlValueParser: options passed when instantiating url-value-parser. This is the easiest way to customize which parts of the URL should be replaced with "#val". See the docs of url-value-parser module for details.
- formatStatusCode:
function(res)
producing final status code from expressres
object, e.g. you can combine200
,201
and204
to just2xx
. - transformLabels:
function(labels, req, res)
transforms the labels object, e.g. setting dynamic values to customLabels
- 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 - promRegistry: Optional
promClient.Registry
instance to attach metrics to. Defaults to globalpromClient.register
.
Let's say you want to have latency statistics by URL path,
e.g. separate metrics for /my-app/user/
, /products/by-category
etc.
Just taking req.path
as a label value won't work as IDs are often part of the URL,
like /user/12352/profile
. So what we actually need is a path template.
The module tries to figure out what parts of the path are values or IDs,
and what is an actual path. The example mentioned before would be
normalized to /user/#val/profile
and that will become the value for the label.
These conversions are handled by normalizePath
function.
You can extend this magical behavior by providing
additional RegExp rules to be performed,
or override normalizePath
with your own function.
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
]
}
}));
app.use(promBundle(/* options? */));
// let's reuse the existing one and just add some
// functionality on top
const originalNormalize = promBundle.normalizePath;
promBundle.normalizePath = (req, opts) => {
const path = originalNormalize(req, opts);
// count all docs as one path, but /docs/login as a separate one
return (path.match(/^\/docs/) && !path.match(/^\/login/)) ? '/docs/*' : path;
};
For more details:
- url-value-parser - magic behind automatic path normalization
- normalizePath.js - source code for path processing
setup std. metrics but exclude up
-metric:
const express = require("express");
const app = express();
const promBundle = require("express-prom-bundle");
// calls to this route will not appear in metrics
// because it's applied before promBundle
app.get("/status", (req, res) => res.send("i am healthy"));
// register metrics collection for all routes
// ... except those starting with /foo
app.use("/((?!foo))*", promBundle({includePath: true}));
// this call will NOT appear in metrics,
// because express will skip the metrics middleware
app.get("/foo", (req, res) => res.send("bar"));
// calls to this route will appear in metrics
app.get("/hello", (req, res) => res.send("ok"));
app.listen(3000);
See an advanced example on github
const promBundle = require("express-prom-bundle");
const Koa = require("koa");
const c2k = require("koa-connect");
const metricsMiddleware = promBundle({/* options */ });
const app = new Koa();
app.use(c2k(metricsMiddleware));
app.use(/* your middleware */);
app.listen(3000);
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.
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`);
}
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.
{
"middleware": {
"expressPromBundle": {
"route": "/((?!status|favicon.ico|robots.txt))*",
"priority": 0,
"module": {
"name": "express-prom-bundle",
"arguments": [
{
"includeMethod": true,
"includePath": true,
"buckets": [0.1, 1, 5],
"promClient": {
"collectDefaultMetrics": {
}
},
"urlValueParser": {
"minHexLength": 5,
"extraMasks": [
"^[0-9]+\\.[0-9]+\\.[0-9]+\\.[0-9]+$"
]
}
}
]
}
}
}
}
MIT