Code Monkey home page Code Monkey logo

express-prom-bundle's Introduction

build status Coverage Status license NPM version

express prometheus bundle

Express middleware with popular prometheus metrics in one bundle. It's also compatible with koa v1 and v2 (see below).

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/summary labeled with status_code, method and path

Install

npm install prom-client express-prom-bundle

Sample Usage

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);

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.

Options

Which labels to include in http_request_duration_seconds metric:

  • includeStatusCode: HTTP status code (200, 400, 404 etc.), default: true
  • includeMethod: HTTP method (GET, PUT, ...), default: false
  • includePath: URL path (see important details below), default: false
  • customLabels: an object containing extra labels, e.g. {project_name: 'hello_world'}. Most useful together with transformLabels callback, otherwise it's better to use native Prometheus relabeling.
  • includeUp: include an auxiliary "up"-metric which always returns 1, default: true
  • metricsPath: replace the /metrics route with a regex or exact string. Note: it is highly recommended to just stick to the default
  • metricType: histogram/summary selection. See more details below
  • httpDurationMetricName: Allows you change the name of HTTP duration metric, default: http_request_duration_seconds.

metricType option

Two metric types are supported for http_request_duration_seconds metric:

Additional options for histogram:

  • buckets: buckets used for the http_request_duration_seconds histogram

Additional options for summary:

  • percentiles: percentiles used for http_request_duration_seconds summary
  • ageBuckets: ageBuckets configures how many buckets we have in our sliding window for the summary
  • maxAgeSeconds: the maxAgeSeconds will tell how old a bucket can be before it is reset
  • pruneAgedBuckets: When enabled, timed out buckets will be removed entirely. By default, buckets are reset to 0.

Transformation callbacks

  • normalizePath: function(req) or Array
    • if function is provided, then it should generate path value from express req
    • if array is provided, then it should be an array of tuples [regex, replacement]. The regex can be a string and is automatically converted into JS regex.
    • ... see more details in the section below
  • urlValueParser: options passed when instantiating url-value-parser. 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 express res object, e.g. you can combine 200, 201 and 204 to just 2xx.
  • transformLabels: function(labels, req, res) transforms the labels object, e.g. setting dynamic values to customLabels
  • urlPathReplacement: replacement string for the values (default: "#val")

Other options

  • autoregister: if /metrics endpoint should be registered (default: true)

  • promClient: options for promClient startup, e.g. collectDefaultMetrics. This option was added to keep express-prom-bundle runnable using confit (e.g. with kraken.js) without writing any JS code, see advanced example

  • 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:

    {
      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

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.

Example 1 (add custom RegExp):

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):

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:

Example 3 (return express route definition):

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:

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

koa v2 example

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);

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.

const cluster = require('cluster');
const promBundle = require('express-prom-bundle');
const promClient = require('prom-client');
const numCPUs = Math.max(2, require('os').cpus().length);
const express = require('express');

if (cluster.isMaster) {
    for (let i = 1; i < numCPUs; i++) {
        cluster.fork();
    }

    const metricsApp = express();
    metricsApp.use('/metrics', promBundle.clusterMetrics());
    metricsApp.listen(9999);

    console.log('cluster metrics listening on 9999');
    console.log('call localhost:9999/metrics for aggregated metrics');
} else {
    new promClient.AggregatorRegistry();
    const app = express();
    app.use(promBundle({
        autoregister: false, // disable /metrics for single workers
        includeMethod: true
    }));
    app.use((req, res) => res.send(`hello from pid ${process.pid}\n`));
    app.listen(3000);
    console.log(`worker ${process.pid} listening on 3000`);
}

using with kraken.js

Here is meddleware config sample, which can be used in a standard kraken.js application. 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]+$"
              ]
            }
          }
        ]
      }
    }
  }
}

License

MIT

express-prom-bundle's People

Contributors

aaronleesmith avatar ankon avatar anxolin avatar ath88 avatar caraboides avatar cellaryllis avatar cerodriguezl avatar cliedeman avatar daniyel avatar dependabot[bot] avatar diegoximenes avatar disjunction avatar dpc avatar ericuldall avatar fauxfaux avatar gabordobrei avatar gabrielcastro avatar ineentho avatar jcreamer898 avatar jonaskello avatar matheuslc avatar modelga avatar paulopaquielli avatar pvradarkp avatar raszi avatar saifulwebid avatar sirius226 avatar swimmadude66 avatar thewizarodofoz avatar yacineb avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar

express-prom-bundle's Issues

maxAgeSeconds not part of "opts" type

I am in a Typescript project and try to create a promBundle middleware using the maxAgeSeconds config for the "summary".
It seems that the maxAgeSeconds is not part of the interface type for express_prom_bundle.Opts. Therefore it is not possible to create with correct types.

const metricsMiddleware = promBundle({
        includeMethod: true,
        includePath: true,
        autoregister: false,
        metricType: 'summary',
        maxAgeSeconds: 600  // <-- Typing error
    });

Dealing with SPA

Description

Currently, I'm trying to add this library to my express server that is serving some static content pages generated by VueJS with router. When I access any other URL on express it works just fine, but it is unable to handle those static pages generated by VueJS.

Expected Behaviour

The middleware should be able to identify when express is forwarding requests to this static content and intercept the url parameters generated by VueRouter and generate metrics from it.

Current Behaviour

When accessing urls from this static VueJS file is being handled as / only.

Caveats

I noticed that the library is able to identify the requests when you force a request to browser, like hitting CTRL + R or F5, but if you try to navigate using the VueRouter links, even if the URL itself updates, it doesn't identify it.

Sample Code.

Bellow is my example code that I'm using. I'm doing some normalizePath to remove individual calls to static content files such as images, css and bundled JS files. Also, I used the history property on my vue router to remove that /#/ from the url, as I thought it was due to that, but no change at all.

var express = require('express');
var history = require('connect-history-api-fallback');
const promBundle = require("express-prom-bundle");

var app = express();

const metricsMiddleware = promBundle({
	includeMethod: true,
	includeStatusCode: true,
	includePath: true,
	includeUp: false
	,
	normalizePath:
		[	
			['^/(js|css|img|favicon.ico)', '/static'],
			['^/static/.*', '/static'],
			['^/#.*', '/edit1']
		]
});

app.use(metricsMiddleware);

const staticFileMiddleware = express.static('dist');

app.use(staticFileMiddleware);
app.use(history({ index: '/dist/index.html', verbose: true}));
app.use(staticFileMiddleware);

app.listen(3000, function () {
	console.log('Example app listening on port 3000!');
});

Allow a path whitelist option

Thanks for the extremely useful middlware!

I've run into a potential issue when using it, however.

When turning on the includePath option, I don't see a convenient way to allow only the paths I want. This is important to have for security reasons. A malicious caller could flood your endpoint with randomly generated urls that would 404, but also create metrics for each of those paths (which could get really expensive in your cloud provider).

I see a way to exclude paths via excludeRoutes and that I can specify anything I want with bypass, however, I don't see something more convenient like a whitelistRoutes, allowRoutes, includeRoutes, or onlyIncludeRoutes.

I see that a whitelist and blacklist used to be in the code, but was replaced with only excludeRoutes: c6d5964

How to workaround with bypass:

app.use(promBundle({
    bypass: req => {
        const onlyIncludeRoutes = ['/foo', '/foo/', /^\/bar\/?$/];
        return !onlyIncludeRoutes.some(route => {
            if (route instanceof RegExp) {
                return route.test(req.url);
            } else {
                return route === req.url;
            }
        })
    }
}));

Better workaround:

app.use(['/foo', '/bar'], promBundle({}));

Proposed feature: just add this in-between bypass and excludeRoutes:

if (opts.onlyIncludeRoutes && !matchVsRegExps(path, opts.onlyIncludeRoutes)) {
    return next();
}

Thanks again for awesome middleware!

Missing support for node cluster

When running a node API with cluster, the bundle will only report metrics for the worker which served the specific /metrics request.

prom-client addressed this in prom-client#82. It seems this middleware should have another config option which indicates usage in a clustered environment, and reports aggregated metrics instead.

Normalizing URL path: Replace #val with custom values/attributes

Current values using the 'express-prom-bundle' module:

attribute: path
Value:
"/api/v1/engagements/#val/features"
"/api/v1/engagements/#val"
"/api/v1/engagements/#val/data/workspace/assets/#val/unzip"

But we are expecting the metrics report to come up with the below values:

attribute: path
Value:
"/api/v1/engagements/{engagementId}/features"
"/api/v1/engagements/{engagementId"
"/api/v1/engagements/{engagementId}/data/workspace/assets/{assetId}/unzip"

I already tried the below normalizePath expressions:

normalizePath: [
['^/api/v1/engagements/./features', '/api/v1/engagements/{engagementId}/features'],
['^/api/v1/engagements/.
', '/api/v1/engagements/{engagementId}'],
]

Intentional breaking changes?

Is it intentional to accept all breaking changes from prom-client as a peerDependency? In the package.json,

  "peerDependencies": {
    "prom-client": ">=12.0.0"
  },

My build started to fail a because the following new metrics were introduced in prom-client 14.1.0.

nodejs_active_resources
nodejs_active_resources_total

Because of the peerDependency, prom-client v14 was installed (maybe v13 that was specified as a devDependency was intented).

At any rate, prom-client could introduce breaking changes that will cause express-prom-bundle to fail, and maybe already has in 14 in a way that I am not testing.

Is it possible to override the normalizePath from the config?

Hello,
In the documentation it says that one needs to override the original normalizePath function to add custom logic.

Is it possible to do so also by specifying a normalizePath key in the config passed to the promBundle function call?
for example: (my config, in the code i call promBundle(config.get("prometheus")))

    prometheus:
        includeMethod: true
        includePath: true
        promClient:
            collectDefaultMetrics:
                timeout: 1000
         normalizePath:
             '\/auth\/token\/.*$': '/auth/token/*' => this will get magically parsed and added to the paths-to-normalize

Thank you,
Yarden

No promClient and metricsMiddleware in TypeScript definitions

There are no promClient and metricsMiddleware in TypeScript definitions.

const metricsRequestMiddleware = promBundle({ ... });
const { promClient, metricsMiddleware } = metricsRequestMiddleware;
//      ^^^^^^^^^^  ^^^^^^^^^^^^^^^^^
// Property 'promClient' does not exist on type 'RequestHandler<ParamsDictionary>'.
// Property 'metricsMiddleware' does not exist on type 'RequestHandler<ParamsDictionary>'.

Should prom-client be a peer dependency?

I recently started using express-prom-bundle, but ran into a problem:

  1. My server imported prom-client directly and exported some metrics on /metrics.
  2. I added express-prom-bundle, but set autoregister to false, because I had already defined a /metrics route myself.
  3. express-prom-bundle happily registered its metrics. Unfortunately for me, it installed its metrics on its own instance of the prom-client module. This meant that when I called promClient.register.metrics() in my own route, I didn't get the metrics from express-prom-bundle, because they were installed on the wrong global registry!

I managed to fix this by forking express-prom-bundle and turning prom-client into a peer dependency. I think this is the right way to go: If prom-client is a peer dependency, express-prom-bundle will use the same version of prom-client as the top-level Node server. But it's not backwards compatible.

(The fork is here: https://github.com/askspoke/express-prom-bundle.)

What do you think?

Question: Prefixing metrics

Is it possible to prefix the collected metrics (e.g. with application name) similarly to how default metrics can be prefixed in prom-client?

promtool reports errors

Noticed the scrape endpoint was working with prometheus so ran the prom tool. I got the following output

'nodejs_active_handles_total non-counter metrics should not have "_total" suffix'

As this is standard stuff how is this working for everyone else?

Cluster: Operation timed out.

I'm trying to run the bundle in a cluster but I get a time out error from prom-client.
I'm using the same example from the readme.

const cluster = require('cluster');
const promBundle = require('express-prom-bundle');
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`);
}

GET http://localhost:9999/metrics

Error: Operation timed out.
    at Timeout._onTimeout (/Users/xxx/git/bim/node_modules/prom-client/lib/cluster.js:59:18)
    at listOnTimeout (internal/timers.js:551:17)
    at processTimers (internal/timers.js:494:7)

Node version: v14.8.0

updating dependencies

Hello,

Is there a blocking reason why dependencies, like prom-client, are outdated? (current version is 6, package version is 3)

If no, would you like a pull request on that matter?

Thanks

Question: How do I configure a different port for the metrics

Hello ! First of all thank you for you amazing work on this repo :)

In my company we are using Prometheus for all the java projects and I wanted to achieve the same for our node servers as well.

All the endpoints of metrics however has to be in a specific port (different from the app server). Something like my-url.com:9000/metrics.

Is it possible to achieve that with this repo?

Thank you :)

Prom bundle configuration should take registry as an option.

Currently, this package always uses the ES6 global import of client.register to register metrics (the default). It should also support custom registries as part of the configuration. This is very helpful in testing and larger apps where multiple registries are used.

Configurable endpoints Requirement for metrices

In case of working with different packages to get the metrices of node application , there is a need to have a configurable endpoint of collection of metrices instead of default one /metrices

localhost/matrices : It should be configurable

Missing `promClient` type in the `express_prom_bundle` interface

The type definition for express_prom_bundle is as follows:

interface express_prom_bundle {
  normalizePath: express_prom_bundle.NormalizePathFn;
  normalizeStatusCode: express_prom_bundle.NormalizeStatusCodeFn;
}

whereas, in the module, a promClient instance is also exposed:

// 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;

The same is true for the Middleware interface. I am looking to inject custom metrics alongside the bundle, so that a single metric middleware would provide prometheus server with the metrics. If there is another way to achieve the same, please provide some example for the same.

Thanks

Configurable replacement option in normalize path

The normalize path function does not take any option for replacement value for path. The path values always gets replaced with #value.
Can this be configurable as an option so the path values can be replaced with some other replacement such as #id.

excludeRoutes is missing from the TypeScript types

While working on #90, I noticed that the excludeRoutes option is in the javascript code, but not in the TS types (so it can error when you try to use it in TS w/o doing some trickery -- depending on your TS settings).

seconds vs milliseconds

Wondering if you could help me figure this out. I am using express prom bundle deployed in a cluster mode.

I am trying to determine what the average response time is for a particular path. This is the query I have:

sum (rate(http_request_duration_seconds_sum{job="myapp",path="/api/helloworld"}[1m])) / sum (rate(http_request_duration_seconds_count{job="myapp",path=~"/api/helloworld"}[1m]))

I plot this as a line chart in Grafana with y-axis units set to seconds. However this results in absurdly large response times. The times appear more realistic if the unit is changed to milliseconds. If the values are aggregated in second buckets why does the choice of milliseconds as the unit make more sense?

Please delete/close the issue if this is not an appropriate forum for this.

Allow custom tags

It would be a great feature to allow custom tags when applying this middleware. Perhaps the end user has some environment variables they would like to be scraped by prometheus to improve dashboard display granularity.

Example:

const promBundle = require("express-prom-bundle");
const app = require("express")();
var customTags = {
  "zone": process.env.ZONE,
  "env": "production",
  "version": "1.0.0"
};
const metricsMiddleware = promBundle({includeMethod: true, includeCustomTags: customTags});

app.use(metricsMiddleware);
app.use(/* your middleware */);
app.listen(3000);

Then in our output it would be:

http_request_duration_seconds_count{status_code="200",method="GET",path="/",zone="us-central1-b",env="production",version="1.0.0"} 1

Is this something that would be accepted as a PR?

Example Dasboard

May be somebody will find it useful

Screen Shot 2019-06-06 at 04 19 34

chart.json
{
  "__inputs": [
    {
      "name": "DS_PROMETHEUS",
      "label": "prometheus",
      "description": "",
      "type": "datasource",
      "pluginId": "prometheus",
      "pluginName": "Prometheus"
    }
  ],
  "__requires": [
    {
      "type": "grafana",
      "id": "grafana",
      "name": "Grafana",
      "version": "6.0.0"
    },
    {
      "type": "panel",
      "id": "graph",
      "name": "Graph",
      "version": "5.0.0"
    },
    {
      "type": "datasource",
      "id": "prometheus",
      "name": "Prometheus",
      "version": "5.0.0"
    }
  ],
  "annotations": {
    "list": [
      {
        "builtIn": 1,
        "datasource": "-- Grafana --",
        "enable": true,
        "hide": true,
        "iconColor": "rgba(0, 211, 255, 1)",
        "name": "Annotations & Alerts",
        "type": "dashboard"
      }
    ]
  },
  "editable": true,
  "gnetId": null,
  "graphTooltip": 0,
  "id": null,
  "iteration": 1559766445973,
  "links": [],
  "panels": [
    {
      "aliasColors": {},
      "bars": false,
      "dashLength": 10,
      "dashes": false,
      "datasource": "${DS_PROMETHEUS}",
      "fill": 1,
      "gridPos": {
        "h": 8,
        "w": 12,
        "x": 0,
        "y": 0
      },
      "id": 2,
      "legend": {
        "avg": false,
        "current": false,
        "max": false,
        "min": false,
        "show": true,
        "total": false,
        "values": false
      },
      "lines": true,
      "linewidth": 1,
      "links": [],
      "nullPointMode": "null",
      "paceLength": 10,
      "percentage": false,
      "pointradius": 2,
      "points": false,
      "renderer": "flot",
      "seriesOverrides": [],
      "stack": false,
      "steppedLine": false,
      "targets": [
        {
          "expr": "sum by (status_code)(rate(http_request_duration_seconds_count{service=\"$service\"}[1m]))",
          "format": "time_series",
          "intervalFactor": 1,
          "legendFormat": "HTTP {{status_code}}",
          "refId": "A"
        }
      ],
      "thresholds": [],
      "timeFrom": null,
      "timeRegions": [],
      "timeShift": null,
      "title": "Response Codes per Minute",
      "tooltip": {
        "shared": true,
        "sort": 0,
        "value_type": "individual"
      },
      "type": "graph",
      "xaxis": {
        "buckets": null,
        "mode": "time",
        "name": null,
        "show": true,
        "values": []
      },
      "yaxes": [
        {
          "format": "short",
          "label": null,
          "logBase": 1,
          "max": null,
          "min": null,
          "show": true
        },
        {
          "format": "short",
          "label": null,
          "logBase": 1,
          "max": null,
          "min": null,
          "show": true
        }
      ],
      "yaxis": {
        "align": false,
        "alignLevel": null
      }
    },
    {
      "aliasColors": {},
      "bars": false,
      "dashLength": 10,
      "dashes": false,
      "datasource": "${DS_PROMETHEUS}",
      "fill": 1,
      "gridPos": {
        "h": 8,
        "w": 12,
        "x": 12,
        "y": 0
      },
      "id": 8,
      "legend": {
        "avg": false,
        "current": false,
        "max": false,
        "min": false,
        "show": true,
        "total": false,
        "values": false
      },
      "lines": true,
      "linewidth": 1,
      "links": [],
      "nullPointMode": "null",
      "paceLength": 10,
      "percentage": false,
      "pointradius": 2,
      "points": false,
      "renderer": "flot",
      "seriesOverrides": [],
      "stack": false,
      "steppedLine": false,
      "targets": [
        {
          "expr": "sum(rate(http_request_duration_seconds_sum{service=\"$service\"}[5m])) by (status_code) /\nsum(rate(http_request_duration_seconds_count{service=\"$service\"}[5m])) by (status_code)",
          "format": "time_series",
          "intervalFactor": 1,
          "legendFormat": "HTTP {{status_code}}",
          "refId": "A"
        }
      ],
      "thresholds": [],
      "timeFrom": null,
      "timeRegions": [],
      "timeShift": null,
      "title": "Average Response Time",
      "tooltip": {
        "shared": true,
        "sort": 0,
        "value_type": "individual"
      },
      "type": "graph",
      "xaxis": {
        "buckets": null,
        "mode": "time",
        "name": null,
        "show": true,
        "values": []
      },
      "yaxes": [
        {
          "format": "short",
          "label": null,
          "logBase": 1,
          "max": null,
          "min": null,
          "show": true
        },
        {
          "format": "short",
          "label": null,
          "logBase": 1,
          "max": null,
          "min": null,
          "show": true
        }
      ],
      "yaxis": {
        "align": false,
        "alignLevel": null
      }
    },
    {
      "aliasColors": {},
      "bars": false,
      "dashLength": 10,
      "dashes": false,
      "datasource": "${DS_PROMETHEUS}",
      "fill": 1,
      "gridPos": {
        "h": 8,
        "w": 12,
        "x": 0,
        "y": 8
      },
      "id": 6,
      "legend": {
        "avg": false,
        "current": false,
        "max": false,
        "min": false,
        "show": true,
        "total": false,
        "values": false
      },
      "lines": true,
      "linewidth": 1,
      "links": [],
      "nullPointMode": "null",
      "paceLength": 10,
      "percentage": false,
      "pointradius": 2,
      "points": false,
      "renderer": "flot",
      "seriesOverrides": [],
      "stack": false,
      "steppedLine": false,
      "targets": [
        {
          "expr": "sum by (method, path)(rate(http_request_duration_seconds_count{service=\"$service\"}[1m]))",
          "format": "time_series",
          "intervalFactor": 1,
          "legendFormat": "{{method}} {{path}}",
          "refId": "A"
        }
      ],
      "thresholds": [],
      "timeFrom": null,
      "timeRegions": [],
      "timeShift": null,
      "title": "Requests Per Minute",
      "tooltip": {
        "shared": true,
        "sort": 0,
        "value_type": "individual"
      },
      "type": "graph",
      "xaxis": {
        "buckets": null,
        "mode": "time",
        "name": null,
        "show": true,
        "values": []
      },
      "yaxes": [
        {
          "format": "short",
          "label": null,
          "logBase": 1,
          "max": null,
          "min": null,
          "show": true
        },
        {
          "format": "short",
          "label": null,
          "logBase": 1,
          "max": null,
          "min": null,
          "show": true
        }
      ],
      "yaxis": {
        "align": false,
        "alignLevel": null
      }
    },
    {
      "aliasColors": {},
      "bars": false,
      "dashLength": 10,
      "dashes": false,
      "datasource": "${DS_PROMETHEUS}",
      "fill": 1,
      "gridPos": {
        "h": 8,
        "w": 12,
        "x": 12,
        "y": 8
      },
      "id": 4,
      "legend": {
        "avg": false,
        "current": false,
        "max": false,
        "min": false,
        "show": true,
        "total": false,
        "values": false
      },
      "lines": true,
      "linewidth": 1,
      "links": [],
      "nullPointMode": "null",
      "paceLength": 10,
      "percentage": false,
      "pointradius": 2,
      "points": false,
      "renderer": "flot",
      "seriesOverrides": [],
      "stack": false,
      "steppedLine": false,
      "targets": [
        {
          "expr": "sum(rate(http_request_duration_seconds_sum{service=\"$service\"}[5m])) by (method, path) /\nsum(rate(http_request_duration_seconds_count{service=\"$service\"}[5m])) by (method, path)",
          "format": "time_series",
          "intervalFactor": 1,
          "legendFormat": "{{method}} {{path}}",
          "refId": "A"
        }
      ],
      "thresholds": [],
      "timeFrom": null,
      "timeRegions": [],
      "timeShift": null,
      "title": "Response Time",
      "tooltip": {
        "shared": true,
        "sort": 0,
        "value_type": "individual"
      },
      "type": "graph",
      "xaxis": {
        "buckets": null,
        "mode": "time",
        "name": null,
        "show": true,
        "values": []
      },
      "yaxes": [
        {
          "format": "short",
          "label": null,
          "logBase": 1,
          "max": null,
          "min": null,
          "show": true
        },
        {
          "format": "short",
          "label": null,
          "logBase": 1,
          "max": null,
          "min": null,
          "show": true
        }
      ],
      "yaxis": {
        "align": false,
        "alignLevel": null
      }
    }
  ],
  "schemaVersion": 18,
  "style": "dark",
  "tags": [],
  "templating": {
    "list": [
      {
        "allValue": null,
        "current": {},
        "datasource": "${DS_PROMETHEUS}",
        "definition": "label_values(http_request_duration_seconds_count, service)",
        "hide": 0,
        "includeAll": true,
        "label": "Service",
        "multi": false,
        "name": "service",
        "options": [],
        "query": "label_values(http_request_duration_seconds_count, service)",
        "refresh": 1,
        "regex": "",
        "skipUrlSync": false,
        "sort": 1,
        "tagValuesQuery": "",
        "tags": [],
        "tagsQuery": "",
        "type": "query",
        "useTags": false
      }
    ]
  },
  "time": {
    "from": "now-6h",
    "to": "now"
  },
  "timepicker": {
    "refresh_intervals": [
      "5s",
      "10s",
      "30s",
      "1m",
      "5m",
      "15m",
      "30m",
      "1h",
      "2h",
      "1d"
    ],
    "time_options": [
      "5m",
      "15m",
      "1h",
      "6h",
      "12h",
      "24h",
      "2d",
      "7d",
      "30d"
    ]
  },
  "timezone": "",
  "title": "Express App",
  "uid": "mwTG5sGWz",
  "version": 4
}

Breaking change in prom-client 13.0.0 returns empty response for /metrics

Hi all! Awesome work with express-prom-bundle. Was giving it a try this weekend and it seems like the latest release of prom-client (13.0.0) broke the /metrics endpoint. Digging into the issue I found that [email protected] has breaking changes to metrics(). It now returns a promise. https://github.com/siimon/prom-client/releases/tag/v13.0.0. When using curl to test the endpoint it responds with an empty reply.

package.json

{
  "name": "express-prom-bundle-with-13-0-0",
  "version": "1.0.0",
  "scripts": {
    "start": "node app.js"
  },
  "dependencies": {
    "express": "^4.17.1",
    "express-prom-bundle": "^6.3.0",
    "prom-client": "^13.0.0"
  }
}

app.js

const express = require('express')
const prometheus = require("express-prom-bundle")

const app = express()

const metricsMiddleware = prometheus({
    includeStatusCode: true,
    includeMethod: true,
    includePath: true
})

app.use(metricsMiddleware)

app.listen(3000)

I believe the issue is in metricsMiddleware https://github.com/jochen-schweizer/express-prom-bundle/blob/master/src/index.js#L125-L128. I'm sort of new to both prom-client and express-prom-bundle and not quite sure if this is the only spot affected by the change but hope that this helps.

const metricsMiddleware = async function(req, res) {
    res.writeHead(200, {'Content-Type': 'text/plain'});
    res.end(await opts.promRegistry.metrics());
};

Package consuming 10gb of virtual memory

Hello,
I'm using this package : express-prom-bundle": "^6.3.6.
Recently i've noticed it's consuming ~10gb of virtual memory alone.

Here's my package.json :
{
"name": "app",
"version": "1.0.0",
"description": "Test Server for Kubernetes Cluster",
"main": "index.js",
"type": "module",
"scripts": {
"test": "echo "Error: no test specified" && exit 1",
"start": "nodemon index.js"
},
"author": "Paula Tanasa",
"license": "ISC",
"dependencies": {
"express": "^4.17.1",
"morgan": "^1.10.0",
"nodemon": "^2.0.7",
"express-prom-bundle": "^6.3.6"
}
}

Here's the 'top' command log:
image

Can you take a look and provide any feedback?
Thank you.

Option to lowercase all paths

Since URLs are case-insensitive by nature, I think it would make sense to have an option to lowercase all paths. I'm aware that this could be done by overriding the normalizePath function, but it's very conveninent to not override it and just pass an array of regexes. I also feel like this would be a very common use-case.

E.g. setting lowercasePaths to true should lowercase all paths before they're passed to the normalizePath function.

prom-client 11.2.0 breaks middleware

When running the latest version of prom-client these metrics disappear:

# HELP up 1 = up, 0 = not up
# TYPE up gauge
up 1

# HELP http_request_duration_seconds duration histogram of http responses labeled with: status_code, method, path
# TYPE http_request_duration_seconds histogram

I haven't had the time to figure out what is new in prom-client. I have set the version to 11.1.3 as a quick fix.

Helped needed...

Hi!

I compared the metrics that came out of this and another project and this project makes alot of sense. I've decided to dump our current implementations and move over to this project - however due to a registry already beingin place (this one - https://www.npmjs.com/package/@opentelemetry/exporter-prometheus) I'm trying to get the PrometheusExporter in as a promRegistry.

Not much joy though.

The error. Type 'PrometheusExporter' is missing the following properties from type 'Registry': metrics, clear, resetMetrics, registerMetric, and 7 more.ts(2740) index.d.ts(37, 5): The expected type comes from property 'promRegistry' which is declared here on type 'Opts'

Have you ever integrated this project with opentelemetries prometheus exporter?

metrics can be attached to specific endpoint programatically, but will raise TS2339

Hi

I am attaching metrics programatically in TypeScript. This used to work fine with a previous version (5.0.2) but with recent updates I now have a TypeScript error:
error TS2339: Property 'metricsMiddleware' does not exist on type 'RequestHandler'.

Here is how I used to do it in 5.0.2:

import expressPromBundle from "express-prom-bundle";
const PROMETHEUS_BUNDLE = expressPromBundle({...})
[...]
expressApp.route("/metrics").get(function (req, res) {
            PROMETHEUS_BUNDLE.metricsMiddleware(req, res);
        });

Here is how I try to do it now (with the error TS2339):

expressApp.use("/metrics", PROMETHEUS_BUNDLE_METRICS_MIDDLEWARE.metricsMiddleware);

This is only a typing issue and does not prevent my server from running correctly.

Authenticate metrics endpoint

I want some basic authorization on the /metrics endpoint so that not everyone can get the data from my application.
Is there a way I can add an authentication callback function as option with the request as a parameter?

Thanks for your help.

extraMasks not working

Hey, I can't seem to get the extraMasks of the urlValueParser working, my setup looks like this -

promBundle({ 
   includeMethod: true, 
   includePath: true, 
   buckets: webRequestsBuckets,
   urlValueParser: {
        extraMasks: [
            /^some-email-regex$/,
        ],
    }
})

The regex is not catching, I know this because I'm using the same UrlValueParser library for my logger in a different area of my code -

Works:

const parser = new UrlValueParser({
    extraMasks: [
       /^some-email-regex$/,
    ],
});
....
parser.replacePathValues(req.originalUrl, '#val')

any ideas why? I saw from your code that the options are being passed, so I can't figure out the issue...

Also, in your includePath example you omit the includePath : true

Unintentional(?) breaking peer dep change in 6.3.1

It seems that the intention of a3c15b1 was to support both prom-client@12 and prom-client@13, but f4677ce updated the peer dependency to "prom-client": "^13.0.0", which makes it a peer dependency error to install the package alongside prom-client@12.

This can be fixed by updating the peer dependency to "prom-client": "^12.0.0 || ^13.0.0" and publishing a new release.

If you did want to restrict the peer dependency range to ^13.0.0, then that should be done in a major version (i.e., [email protected] instead of 6.3.1) because it's generally considered a breaking change to modify peer dependencies in a backwards-incompatible way.

Need a way to exclude certain routes in the configuration

Right now to avoid certain routes to be excludes from the metrics - we need to do (as per documentation):
app.use("/((?!foo))*", promBundle({includePath: true}));

It is a bit redundant in our use case as we are using this bundle inside a wrapper and using that wrapper into couple of node apps and we need to exclude multiple routes to be entered into metrics.

It would have been a great thing if somehow the excluded routes can be put inside this bundle configuration object. Or is it something existing and I have missed?

Add option to parametrize /metrics path

Hi,

I think it would be good to have an option to parametrize metrics path during promBundle initialization. I know this is possible by the autoregister set to false and by adding own routing but in this case own routing path is added also to prometheus metrics.

Suggested configuration example:

const metricsMiddleware = promBundle({
  includeMethod: true,
  includePath: true,
  autoregister: true,
  metricsPath: '/prometheus' // instead of /metrics
})

prom-client out of date

Hi.

I want to send custom metrics using expressPromBundle.promClient.Gauge for example.

The API has changed, and now works differently. between the latest stable and 6.3.0

percentiles are not exported

I configured prom-bundle this way:

const prometheusMetricsMiddleware = promBundle({
  includeMethod: true,
  includePath: true,
  percentiles: [0.5, 0.75, 0.9, 0.95]
});

But i see no percentiles in /metrics only the default buckets like:

http_request_duration_seconds_bucket{le="0.003",status_code="200",method="GET",path="/entd/fragment/register-head"} 12021
http_request_duration_seconds_bucket{le="0.03",status_code="200",method="GET",path="/entd/fragment/register-head"} 12102
http_request_duration_seconds_bucket{le="0.1",status_code="200",method="GET",path="/entd/fragment/register-head"} 12188
http_request_duration_seconds_bucket{le="0.3",status_code="200",method="GET",path="/entd/fragment/register-head"} 12188
http_request_duration_seconds_bucket{le="1.5",status_code="200",method="GET",path="/entd/fragment/register-head"} 12188
http_request_duration_seconds_bucket{le="10",status_code="200",method="GET",path="/entd/fragment/register-head"} 12188
http_request_duration_seconds_bucket{le="+Inf",status_code="200",method="GET",path="/entd/fragment/register-head"} 12188

Exclude /metrics route

Hello,
First of all thank you so much for this library, I appreciate you hard word,
Now i don't want to display /metrics request in my grafana dashboard, Is there any way to exclude /metrics route
I tried app.use("/((?!metrics))*", promBundle({includePath: true}));, but It blocks the /metrics and returns no route found which was the only access point (target) for my Prometheus server to access metrics data.
I just want to exclude "/metrics" path in my collection which is polluting my app's APIs.
Thanks

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.