UNPKG

prometheus-api-metrics

Version:

API and process monitoring with Prometheus for Node.js micro-service

129 lines (107 loc) 4.95 kB
const Prometheus = require('prom-client'); require('pkginfo')(module, ['name']); const debug = require('debug')(module.exports.name); const utils = require('./utils'); class ExpressMiddleware { constructor(setupOptions) { this.setupOptions = setupOptions; } _collectDefaultServerMetrics(timeout) { const NUMBER_OF_CONNECTIONS_METRICS_NAME = 'expressjs_number_of_open_connections'; this.setupOptions.numberOfConnectionsGauge = Prometheus.register.getSingleMetric(NUMBER_OF_CONNECTIONS_METRICS_NAME) || new Prometheus.Gauge({ name: NUMBER_OF_CONNECTIONS_METRICS_NAME, help: 'Number of open connections to the Express.js server' }); if (this.setupOptions.server) { setInterval(this._getConnections.bind(this), timeout).unref(); } } _getConnections() { if (this.setupOptions && this.setupOptions.server) { this.setupOptions.server.getConnections((error, count) => { if (error) { debug('Error while collection number of open connections', error); } else { this.setupOptions.numberOfConnectionsGauge.set(count); } }); } } _handleResponse(req, res) { const responseLength = parseInt(res.get('Content-Length')) || 0; const route = this._getRoute(req); if (route && utils.shouldLogMetrics(this.setupOptions.excludeRoutes, route)) { const labels = { method: req.method, route, code: res.statusCode, ...this.setupOptions.extractAdditionalLabelValuesFn(req, res) }; this.setupOptions.requestSizeHistogram.observe(labels, req.metrics.contentLength); req.metrics.timer(labels); this.setupOptions.responseSizeHistogram.observe(labels, responseLength); debug(`metrics updated, request length: ${req.metrics.contentLength}, response length: ${responseLength}`); } } _getRoute(req) { let route = req.baseUrl; if (req.route) { if (req.route.path !== '/') { route = route ? route + req.route.path : req.route.path; } if (!route || route === '' || typeof route !== 'string') { route = req.originalUrl.split('?')[0]; } else { const splittedRoute = route.split('/'); const splittedUrl = req.originalUrl.split('?')[0].split('/'); const routeIndex = splittedUrl.length - splittedRoute.length + 1; const baseUrl = splittedUrl.slice(0, routeIndex).join('/'); route = baseUrl + route; } if (this.setupOptions.includeQueryParams === true && Object.keys(req.query).length > 0) { route = `${route}?${Object.keys(req.query).sort().map((queryParam) => `${queryParam}=<?>`).join('&')}`; } } // nest.js - build request url pattern if exists if (typeof req.params === 'object') { Object.keys(req.params).forEach((paramName) => { route = route.replace(req.params[paramName], ':' + paramName); }); } // this condition will evaluate to true only in // express framework and no route was found for the request. if we log this metrics // we'll risk in a memory leak since the route is not a pattern but a hardcoded string. if (!route || route === '') { // if (!req.route && res && res.statusCode === 404) { route = 'N/A'; } return route; } async middleware(req, res, next) { if (!this.setupOptions.server && req.socket) { this.setupOptions.server = req.socket.server; this._collectDefaultServerMetrics(this.setupOptions.defaultMetricsInterval); } const routeUrl = req.originalUrl || req.url; if (routeUrl === this.setupOptions.metricsRoute) { debug('Request to /metrics endpoint'); res.set('Content-Type', Prometheus.register.contentType); return res.end(await Prometheus.register.metrics()); } if (routeUrl === `${this.setupOptions.metricsRoute}.json`) { debug('Request to /metrics endpoint'); return res.json(await Prometheus.register.getMetricsAsJSON()); } req.metrics = { timer: this.setupOptions.responseTimeHistogram.startTimer(), contentLength: parseInt(req.get('content-length')) || 0 }; debug(`Set start time and content length for request. url: ${routeUrl}, method: ${req.method}`); res.once('finish', () => { debug('on finish.'); this._handleResponse(req, res); }); return next(); }; } module.exports = ExpressMiddleware;