@mojaloop/central-services-metrics
Version:
Shared code for metrics generation
351 lines (345 loc) • 17.8 kB
JavaScript
/*****
License
--------------
Copyright © 2020-2025 Mojaloop Foundation
The Mojaloop files are made available by the Mojaloop Foundation under the Apache License, Version 2.0 (the "License") and you may not use these files except in compliance with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, the Mojaloop files are distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
Contributors
--------------
This is the official list of the Mojaloop project contributors for this file.
Names of the original copyright holders (individuals or organizations)
should be listed with a '*' in the first column. People who have
contributed from an organization can be listed under the organization
that actually holds the copyright for their contributions (see the
Mojaloop Foundation for an example). Those individuals should have
their names indented and be marked with a '-'. Email address can be added
optionally within square brackets <email>.
* Mojaloop Foundation
- Name Surname <name.surname@mojaloop.io>
- Pedro Barreto <pedrob@crosslaketech.com>
- Rajiv Mothilal <rajivmothilal@gmail.com>
- Miguel de Barros <miguel.debarros@modusbox.com>
- Shashikant Hirugade <shashikant.hirugade@modusbox.com>
- Kevin Leyow <kevin.leyow@infitx.com>
--------------
******/
'use strict';
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __generator = (this && this.__generator) || function (thisArg, body) {
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
function verb(n) { return function (v) { return step([n, v]); }; }
function step(op) {
if (f) throw new TypeError("Generator is already executing.");
while (g && (g = 0, op[0] && (_ = 0)), _) try {
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
if (y = 0, t) op = [op[0] & 2, t.value];
switch (op[0]) {
case 0: case 1: t = op; break;
case 4: _.label++; return { value: op[1], done: false };
case 5: _.label++; y = op[1]; op = [0]; continue;
case 7: op = _.ops.pop(); _.trys.pop(); continue;
default:
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
if (t[2]) _.ops.pop();
_.trys.pop(); continue;
}
op = body.call(thisArg, _);
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
}
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Metrics = void 0;
var client = require("prom-client");
/** Wrapper class for prom-client. */
var Metrics = /** @class */ (function () {
function Metrics() {
var _this = this;
/** To make sure the setup is run only once */
this._alreadySetup = false;
/** The options passed to the setup */
this._options = { prefix: '', timeout: 0, maxConnections: 0, maxRequestsPending: 0 };
/** Object containing the default registry */
this._register = client.register;
/** Object containing the histogram values */
this._histograms = {};
/** Object containing the summaries values */
this._summaries = {};
/** Object containing the counter values */
this._counters = {};
/** Object containing the gauge values */
this._gauges = {};
/**
* Setup the prom client for collecting metrics using the options passed
*/
this.setup = function (options) {
if (_this._alreadySetup) {
client.AggregatorRegistry.setRegistries(_this.getDefaultRegister());
return false;
}
_this.getDefaultRegister().clear();
_this._options = options;
// map the options to the normalised options specific to the prom-client
var normalisedOptions = {
prefix: _this._options.prefix,
timeout: _this._options.timeout
};
if (_this._options.defaultLabels !== undefined) {
client.register.setDefaultLabels(_this._options.defaultLabels);
}
// configure default metrics
if (options.defaultMetrics !== false)
client.collectDefaultMetrics(normalisedOptions);
// set default registry
// client.AggregatorRegistry.setRegistries(this.getDefaultRegister())
_this._register = client.register;
// set setup flag
_this._alreadySetup = true;
_this._setupDefaultServiceMetrics();
// return true if we are setup
return true;
};
this._setupDefaultServiceMetrics = function () {
_this.getCounter('errorCount', 'Error count', ['code', 'system', 'operation', 'step', 'context', 'expected'], false);
_this.getCounter('app_critical_total', 'Total times app entered critical health', ['service'], false);
};
/**
* Get the histogram values for given name
*/
// getHistogram = (name: string, help?: string, labelNames?: string[], buckets: number[] = [0.010, 0.050, 0.1, 0.5, 1, 2, 5]): client.Histogram<string> => { // <-- required for Prom-Client v12.x
this.getHistogram = function (name, help, labelNames, buckets) {
if (buckets === void 0) { buckets = [0.010, 0.050, 0.1, 0.5, 1, 2, 5]; }
try {
if (_this._histograms[name] != null) {
return _this._histograms[name];
}
_this._histograms[name] = new client.Histogram({
name: "".concat(_this.getOptions().prefix).concat(name),
help: (help != null ? help : "".concat(name, "_histogram")),
labelNames: labelNames,
buckets: buckets // this is in seconds - the startTimer().end() collects in seconds with ms precision
});
return _this._histograms[name];
}
catch (e) {
throw new Error("Couldn't get metrics histogram for ".concat(name));
}
};
/**
* Get the summary for given name
*/
// getSummary = (name: string, help?: string, labelNames?: string[], percentiles: number[] = [ 0.01, 0.05, 0.5, 0.9, 0.95, 0.99, 0.999], maxAgeSeconds: number = 600, ageBuckets: number = 5): client.Summary<string> => { // <-- required for Prom-Client v12.x
this.getSummary = function (name, help, labelNames, percentiles, maxAgeSeconds, ageBuckets) {
if (percentiles === void 0) { percentiles = [0.01, 0.05, 0.5, 0.9, 0.95, 0.99, 0.999]; }
if (maxAgeSeconds === void 0) { maxAgeSeconds = 600; }
if (ageBuckets === void 0) { ageBuckets = 5; }
try {
if (_this._summaries[name] != null) {
return _this._summaries[name];
}
_this._summaries[name] = new client.Summary({
name: "".concat(_this.getOptions().prefix).concat(name),
help: (help != null ? help : "".concat(name, "_summary")),
labelNames: labelNames,
maxAgeSeconds: maxAgeSeconds,
percentiles: percentiles,
ageBuckets: ageBuckets
});
return _this._summaries[name];
}
catch (e) {
throw new Error("Couldn't get summary for ".concat(name));
}
};
this.getCounter = function (name, help, labelNames, prefix) {
if (prefix === void 0) { prefix = true; }
try {
if (_this._counters[name] != null) {
return _this._counters[name];
}
_this._counters[name] = new client.Counter({
name: "".concat(prefix ? _this.getOptions().prefix : '').concat(name),
help: (help != null ? help : "".concat(name, "_counter")),
labelNames: labelNames
});
return _this._counters[name];
}
catch (e) {
throw new Error("Couldn't get counter for ".concat(name));
}
};
this.getGauge = function (name, help, labelNames) {
try {
if (_this._gauges[name] != null) {
return _this._gauges[name];
}
_this._gauges[name] = new client.Gauge({
name: "".concat(_this.getOptions().prefix).concat(name),
help: (help != null ? help : "".concat(name, "_gauge")),
labelNames: labelNames
});
return _this._gauges[name];
}
catch (e) {
throw new Error("Couldn't get gauge for ".concat(name));
}
};
/**
* Get the metrics
*/
this.getMetricsForPrometheus = function () { return __awaiter(_this, void 0, void 0, function () {
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, client.register.metrics()];
case 1: return [2 /*return*/, _a.sent()];
}
});
}); };
/**
* Get the options that are used to setup the prom-client
*/
this.getOptions = function () {
return _this._options;
};
/**
* To check is it the Metrics already initiated
*/
this.isInitiated = function () {
return _this._alreadySetup;
};
this.getDefaultRegister = function () {
return _this._register;
};
this.getClient = function () {
return client;
};
this.plugin = {
name: 'http server metrics',
register: function (server) {
var requestCounter = new client.Counter({
registers: [_this.getDefaultRegister()],
name: 'http_requests_total',
help: 'Total number of http requests',
labelNames: ['method', 'status_code', 'path']
});
var requestDuration = new client.Summary({
registers: [_this.getDefaultRegister()],
name: 'http_request_duration_seconds',
help: 'Duration of http requests',
labelNames: ['method', 'status_code', 'path'],
aggregator: 'average'
});
var requestDurationHistogram = new client.Histogram({
registers: [_this.getDefaultRegister()],
name: 'http_request_duration_histogram_seconds',
help: 'Duration of http requests',
labelNames: ['method', 'status_code', 'path'],
aggregator: 'average'
});
var requests = 0;
var requestsGauge = new client.Gauge({
registers: [_this.getDefaultRegister()],
name: 'http_requests_current',
help: 'Number of requests currently running',
labelNames: ['method']
});
new client.Gauge({
registers: [_this.getDefaultRegister()],
name: 'http_server_start',
help: 'Start indicator for the server'
}).inc();
var first = true;
server.ext('onRequest', function (request, h) {
var _a = _this.getOptions(), _b = _a.maxConnections, maxConnections = _b === void 0 ? 0 : _b, _c = _a.maxRequestsPending, maxRequestsPending = _c === void 0 ? 0 : _c;
if ((maxConnections > 0 || maxRequestsPending > 0) && request.path === '/health') {
if (maxConnections > 0 && connections >= maxConnections) {
return h.response('Max connections reached').code(503).takeover();
}
if (maxRequestsPending > 0 && requests >= maxRequestsPending) {
return h.response('Max requests pending reached').code(503).takeover();
}
}
if (request.path === '/live')
return h.response('OK').code(200).takeover();
if (['/metrics', '/health'].includes(request.path))
return h.continue;
requests++;
requestsGauge.inc({ method: request.method });
return h.continue;
});
server.events.on('response', function (request) {
if (['/metrics', '/health', '/live'].includes(request.path))
return;
requests--;
requestsGauge.dec({ method: request.method });
if (request.route.path === '/{p*}')
return; // unregistered (unknown) route
var path = request.route.path;
var statusCode = String('isBoom' in request.response
? request.response.output.statusCode
: request.response.statusCode);
var duration = Math.max(Math.max(request.info.completed, request.info.responded) - request.info.received, 0);
requestCounter.labels(request.method, statusCode, path).inc();
requestDuration.labels(request.method, statusCode, path).observe(duration);
requestDurationHistogram.labels(request.method, statusCode, path).observe(duration / 1000);
});
var connections = 0;
var connectionsGauge = new client.Gauge({
registers: [_this.getDefaultRegister()],
name: 'http_connections_current',
help: 'Number of connections currently established',
labelNames: ['remote_address']
});
server.listener.on('connection', function (socket) {
var labels = { remote_address: socket.remoteAddress };
connections++;
connectionsGauge.inc(labels);
socket.on('close', function () {
connections--;
connectionsGauge.dec(labels);
});
});
server.route({
method: 'GET',
path: '/metrics',
handler: function (request, h) { return __awaiter(_this, void 0, void 0, function () {
var metrics;
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, this.getMetricsForPrometheus()];
case 1:
metrics = _a.sent();
if (first) {
this.getDefaultRegister().removeSingleMetric('http_server_start');
first = false;
}
return [2 /*return*/, h.response(metrics).code(200).type('text/plain; version=0.0.4')];
}
});
}); },
options: {
tags: ['api', 'metrics'],
description: 'Prometheus metrics endpoint',
id: 'metrics'
}
});
}
};
}
return Metrics;
}());
exports.Metrics = Metrics;
//# sourceMappingURL=metrics.js.map