import promClient, { Gauge, Histogram } from 'prom-client'; import expressPromBundle from 'express-prom-bundle'; import knexPromExporter, { KnexExporterResult } from 'knex-prometheus-exporter'; import fs from 'fs'; import path from 'path'; import type { Router } from 'express'; import type { Knex } from 'knex'; import * as git from './git'; import type { Log } from './types'; interface PrometheusConfig { version: string; namespace: string; name: string; ipAddress: string; pod: string; buildBranch?: string; buildVersion?: string; distPath: string; // Path to the dist folder where info.json is generated } class Prometheus { private _register = new promClient.Registry(); private readonly _config: PrometheusConfig; private readonly _log: Log; private _buildInfoCount: Gauge; private _buildInfoMax: Gauge; private _buildInfoSum: Gauge; private _dynamicallyCreatedHistograms: Record = {}; constructor (config: PrometheusConfig, log?: Log) { this._config = config; this._log = log || console; const { version, namespace, name, ipAddress, pod, distPath, } = this._config; let info; try { info = fs.readFileSync(path.join(distPath, 'info.json'), 'utf8'); } catch (err) { info = '{}'; } const { buildTime, commit, branch, tags, } = JSON.parse(info); const objBuildInfo = { application: name, buildTime, commit, branch, instance: ipAddress, job: name, namespace, pod, service: name, tags, version, }; const buildInfoLabels = ['application', 'buildTime', 'commit', 'branch', 'instance', 'job', 'namespace', 'pod', 'service', 'tags', 'version']; promClient.collectDefaultMetrics({ register: this._register, }); // eslint-disable-next-line no-new new promClient.Counter({ name: 'apiStatus', help: 'Query tracking for API Status', labelNames: ['query', 'status', 'message', 'originalUrl'], registers: [this._register], }); // eslint-disable-next-line no-new new promClient.Counter({ name: 'httpError', help: 'Query tracking for API Status', labelNames: ['name', 'query', 'status', 'message', 'originalUrl'], registers: [this._register], }); this._buildInfoCount = new promClient.Gauge({ help: 'Build Info Count', name: 'buildInfo_count', labelNames: buildInfoLabels, registers: [this._register], }); this._buildInfoCount.set(objBuildInfo, 0); this._buildInfoMax = new promClient.Gauge({ help: 'Build Info Max', name: 'buildInfo_max', labelNames: buildInfoLabels, registers: [this._register], }); this._buildInfoMax.set(objBuildInfo, 0); this._buildInfoSum = new promClient.Gauge({ help: 'Build Info Sum', name: 'buildInfo_sum', labelNames: buildInfoLabels, registers: [this._register], }); this._buildInfoSum.set(objBuildInfo, 0); } metrics () { return this._register.metrics(); } metricsInJson () { return this._register.getMetricsAsJSON(); } getExpressMetricsMiddleware () { // Options described: https://www.npmjs.com/package/express-prom-bundle return expressPromBundle({ includeMethod: true, includePath: true, buckets: [0.1, 1, 5], autoregister: false, promRegistry: this._register, }); } registerKnex (knex: Knex, id: string) { // Some deployments have '-' symbol in id. const sanitizedId = id.replace(/-/g, '_'); return knexPromExporter( knex, { register: this._register, prefix: `knex_${sanitizedId}_`, }, ); } unregisterKnex (exporter: KnexExporterResult) { exporter.off(); } attachToExpress (router: Pick) { router.get('/management/prometheus', async (_, res) => { res.setHeader('content-type', 'text/plain'); res.send(await this.metrics()); }); router.get('/management/metrics', async (_, res) => res.send(await this.metricsInJson())); return router; } buildInfoFile () { try { const branch = this._config.buildBranch ?? git.getCurrentBranch(this._log); const commit = git.getCurrentRevision(this._log); const tags = git.getCurrentTags(this._log); const buildTime = new Date().toISOString(); const version = this._config.buildVersion || this._config.version; const info = { version, buildTime, commit, branch, tags, }; this._log.log('Building info.json: ', this._config.distPath, JSON.stringify(info)); fs.writeFileSync(path.join(this._config.distPath, 'info.json'), JSON.stringify(info)); } catch (error) { this._log.warn('Build info failed.', error); } } getInfoFile () { const filePath = path.join(this._config.distPath, 'info.json'); if (!fs.existsSync(filePath)) { throw new Error('File does not exists'); } return fs.createReadStream(filePath, 'utf8'); } createHistogram (name: string, help: string, labelNames: string[] = []) { if (this._dynamicallyCreatedHistograms[name]) { throw new Error(`Histogram ${name} already exists.`); } if (!labelNames.includes('path')) labelNames.push('path'); this._dynamicallyCreatedHistograms[name] = new promClient.Histogram({ name, help, labelNames, registers: [this._register], buckets: [0.1, 1, 5], }); return this._dynamicallyCreatedHistograms[name]; } startHistogram (metricName: string, labels: Record) { if (!this._dynamicallyCreatedHistograms[metricName]) { throw new Error(`Histogram ${metricName} does not exist.`); } return this._dynamicallyCreatedHistograms[metricName].startTimer(labels); } } export { Prometheus };