@substrate/api-sidecar
Version:
REST service that makes it easy to interact with blockchain nodes built using Substrate's FRAME framework.
191 lines • 8.44 kB
JavaScript
"use strict";
// Copyright 2022-2025 Parity Technologies (UK) Ltd.
// This file is part of Substrate API Sidecar.
//
// Substrate API Sidecar is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
// as new metrics are added in routes they need to be registered here to make them available across the apps
const express_1 = __importDefault(require("express"));
const prom_client_1 = __importDefault(require("prom-client"));
const Log_1 = require("../logging/Log");
const SidecarConfig_1 = require("../SidecarConfig");
const _1 = require(".");
class Metrics_App {
/**
* @param appConfig configuration for app.
*/
constructor({ host, port }) {
this.includeQueryParams = SidecarConfig_1.SidecarConfig.config.METRICS.INCLUDE_QUERYPARAMS;
this.port = port;
this.app = (0, express_1.default)();
this.host = host;
this.registry = new prom_client_1.default.Registry();
this.metrics = {};
this.init();
}
listen() {
const { logger } = Log_1.Log;
this.app.listen(this.port, this.host, () => {
logger.info(`Metrics Server started at http://${this.host}:${this.port}/metrics`);
});
}
getRegisteredMetrics() {
return this.metrics;
}
createMetricByType(prefix = 'sas', metric) {
const prefixedName = prefix + '_' + metric.name;
if (prefixedName in this.metrics) {
return this.metrics[prefixedName];
}
switch (metric.type) {
case "counter" /* MetricType.Counter */: {
const counter = new prom_client_1.default.Counter({
name: prefixedName,
help: metric.help,
labelNames: metric.labels || [],
registers: [this.registry],
});
this.registry.registerMetric(counter);
this.metrics[prefixedName] = counter;
return counter;
}
case "histogram" /* MetricType.Histogram */: {
const histogram = new prom_client_1.default.Histogram({
name: prefixedName,
help: metric.help,
labelNames: metric.labels || [],
registers: [this.registry],
buckets: metric.buckets || [0.1, 0.5, 1, 1.5, 2, 3, 4, 5],
});
this.metrics[prefixedName] = histogram;
return histogram;
}
case "gauge" /* MetricType.Gauge */:
throw new Error('Gauge not implemented');
case "summary" /* MetricType.Summary */:
throw new Error('Summary not implemented');
default:
throw new Error('Unknown metric type');
}
}
getRoute(req) {
var _a, _b;
let route = req.baseUrl;
if (req.route) {
if (req.route.path !== '/') {
route = route ? route + ((_a = req.route) === null || _a === void 0 ? void 0 : _a.path) : (_b = req.route) === null || _b === void 0 ? void 0 : _b.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.includeQueryParams === true && Object.keys(req.query).length > 0) {
route = `${route}?${Object.keys(req.query)
.sort()
.map((queryParam) => `${queryParam}=<?>`)
.join('&')}`;
}
}
if (typeof req.params === 'object') {
Object.keys(req.params).forEach((paramName) => {
route = route.replace(req.params[paramName], ':' + paramName);
});
}
if (!route || route === '') {
// if (!req.route && res && res.statusCode === 404) {
route = 'N/A';
}
return route;
}
preMiddleware() {
return (req, res, next) => {
const tot_requests = this.metrics['sas_http_requests'];
// request count metrics
if (req.originalUrl != '/favicon.ico') {
tot_requests.inc();
}
const request_duration_seconds = this.metrics['sas_request_duration_seconds'];
const end = request_duration_seconds.startTimer();
res.locals.metrics = {
timer: end,
registry: this.metrics,
};
const oldJson = res.json;
res.json = (body) => {
res.locals.body = body;
return oldJson.call(res, body);
};
res.once('finish', () => {
if (res.statusCode >= 400 && req.originalUrl != '/favicon.ico') {
const request_errors = this.metrics['sas_http_request_error'];
request_errors.inc();
}
else if (res.statusCode < 400) {
const request_success = this.metrics['sas_http_request_success'];
request_success.inc();
}
let resContentLength = '0';
if ('_contentLength' in res && res['_contentLength'] != null) {
resContentLength = res['_contentLength'];
}
else if (res.hasHeader('Content-Length')) {
resContentLength = res.getHeader('Content-Length');
}
// Generic Metrics per route (latency, response size, response size to latency ratio)
// response size metrics
const response_size_bytes = this.metrics['sas_response_size_bytes'];
response_size_bytes
.labels({ method: req.method, route: this.getRoute(req), status_code: res.statusCode })
.observe(parseFloat(resContentLength));
// latency metrics
const latency = end({ method: req.method, route: this.getRoute(req), status_code: res.statusCode });
// response size to latency ratio
const response_size_latency_ratio = this.metrics['sas_response_size_bytes_seconds'];
response_size_latency_ratio
.labels({ method: req.method, route: this.getRoute(req), status_code: res.statusCode })
.observe(parseFloat(resContentLength) / latency);
});
next();
};
}
init() {
// Set up
_1.config.metric_list.forEach((metric) => this.createMetricByType('sas', metric));
prom_client_1.default.collectDefaultMetrics({ register: this.registry, prefix: 'sas_' });
// Set up the metrics endpoint
this.app.get('/metrics', (_req, res) => {
void (async () => {
res.set('Content-Type', this.registry.contentType);
res.send(await this.registry.metrics());
})();
});
this.app.get('/metrics.json', (_req, res) => {
void (async () => {
res.set('Content-Type', this.registry.contentType);
res.send(await this.registry.getMetricsAsJSON());
})();
});
}
}
exports.default = Metrics_App;
//# sourceMappingURL=Metrics.js.map