the-tip-top-backend/node_modules/prom-client/lib/registry.js

243 lines
6.0 KiB
JavaScript
Executable File

'use strict';
const { getValueAsString } = require('./util');
class Registry {
static get PROMETHEUS_CONTENT_TYPE() {
return 'text/plain; version=0.0.4; charset=utf-8';
}
static get OPENMETRICS_CONTENT_TYPE() {
return 'application/openmetrics-text; version=1.0.0; charset=utf-8';
}
constructor(regContentType = Registry.PROMETHEUS_CONTENT_TYPE) {
this._metrics = {};
this._collectors = [];
this._defaultLabels = {};
if (
regContentType !== Registry.PROMETHEUS_CONTENT_TYPE &&
regContentType !== Registry.OPENMETRICS_CONTENT_TYPE
) {
throw new TypeError(`Content type ${regContentType} is unsupported`);
}
this._contentType = regContentType;
}
getMetricsAsArray() {
return Object.values(this._metrics);
}
async getMetricsAsString(metrics) {
const metric =
typeof metrics.getForPromString === 'function'
? await metrics.getForPromString()
: await metrics.get();
const name = escapeString(metric.name);
const help = `# HELP ${name} ${escapeString(metric.help)}`;
const type = `# TYPE ${name} ${metric.type}`;
const values = [help, type];
const defaultLabels =
Object.keys(this._defaultLabels).length > 0 ? this._defaultLabels : null;
const isOpenMetrics =
this.contentType === Registry.OPENMETRICS_CONTENT_TYPE;
for (const val of metric.values || []) {
let { metricName = name, labels = {} } = val;
const { sharedLabels = {} } = val;
if (isOpenMetrics && metric.type === 'counter') {
metricName = `${metricName}_total`;
}
if (defaultLabels) {
labels = { ...labels, ...defaultLabels, ...labels };
}
// We have to flatten these separately to avoid duplicate labels appearing
// between the base labels and the shared labels
const formattedLabels = formatLabels(labels, sharedLabels);
const flattenedShared = flattenSharedLabels(sharedLabels);
const labelParts = [...formattedLabels, flattenedShared].filter(Boolean);
const labelsString = labelParts.length ? `{${labelParts.join(',')}}` : '';
let fullMetricLine = `${metricName}${labelsString} ${getValueAsString(
val.value,
)}`;
const { exemplar } = val;
if (exemplar && isOpenMetrics) {
const formattedExemplars = formatLabels(exemplar.labelSet);
fullMetricLine += ` # {${formattedExemplars.join(
',',
)}} ${getValueAsString(exemplar.value)} ${exemplar.timestamp}`;
}
values.push(fullMetricLine);
}
return values.join('\n');
}
async metrics() {
const isOpenMetrics =
this.contentType === Registry.OPENMETRICS_CONTENT_TYPE;
const promises = this.getMetricsAsArray().map(metric => {
if (isOpenMetrics && metric.type === 'counter') {
metric.name = standardizeCounterName(metric.name);
}
return this.getMetricsAsString(metric);
});
const resolves = await Promise.all(promises);
return isOpenMetrics
? `${resolves.join('\n')}\n# EOF\n`
: `${resolves.join('\n\n')}\n`;
}
registerMetric(metric) {
if (this._metrics[metric.name] && this._metrics[metric.name] !== metric) {
throw new Error(
`A metric with the name ${metric.name} has already been registered.`,
);
}
this._metrics[metric.name] = metric;
}
clear() {
this._metrics = {};
this._defaultLabels = {};
}
async getMetricsAsJSON() {
const metrics = [];
const defaultLabelNames = Object.keys(this._defaultLabels);
const promises = [];
for (const metric of this.getMetricsAsArray()) {
promises.push(metric.get());
}
const resolves = await Promise.all(promises);
for (const item of resolves) {
if (item.values && defaultLabelNames.length > 0) {
for (const val of item.values) {
// Make a copy before mutating
val.labels = Object.assign({}, val.labels);
for (const labelName of defaultLabelNames) {
val.labels[labelName] =
val.labels[labelName] || this._defaultLabels[labelName];
}
}
}
metrics.push(item);
}
return metrics;
}
removeSingleMetric(name) {
delete this._metrics[name];
}
getSingleMetricAsString(name) {
return this.getMetricsAsString(this._metrics[name]);
}
getSingleMetric(name) {
return this._metrics[name];
}
setDefaultLabels(labels) {
this._defaultLabels = labels;
}
resetMetrics() {
for (const metric in this._metrics) {
this._metrics[metric].reset();
}
}
get contentType() {
return this._contentType;
}
setContentType(metricsContentType) {
if (
metricsContentType === Registry.OPENMETRICS_CONTENT_TYPE ||
metricsContentType === Registry.PROMETHEUS_CONTENT_TYPE
) {
this._contentType = metricsContentType;
} else {
throw new Error(`Content type ${metricsContentType} is unsupported`);
}
}
static merge(registers) {
const regType = registers[0].contentType;
for (const reg of registers) {
if (reg.contentType !== regType) {
throw new Error(
'Registers can only be merged if they have the same content type',
);
}
}
const mergedRegistry = new Registry(regType);
const metricsToMerge = registers.reduce(
(acc, reg) => acc.concat(reg.getMetricsAsArray()),
[],
);
metricsToMerge.forEach(mergedRegistry.registerMetric, mergedRegistry);
return mergedRegistry;
}
}
function formatLabels(labels, exclude) {
const { hasOwnProperty } = Object.prototype;
const formatted = [];
for (const [name, value] of Object.entries(labels)) {
if (!exclude || !hasOwnProperty.call(exclude, name)) {
formatted.push(`${name}="${escapeLabelValue(value)}"`);
}
}
return formatted;
}
const sharedLabelCache = new WeakMap();
function flattenSharedLabels(labels) {
const cached = sharedLabelCache.get(labels);
if (cached) {
return cached;
}
const formattedLabels = formatLabels(labels);
const flattened = formattedLabels.join(',');
sharedLabelCache.set(labels, flattened);
return flattened;
}
function escapeLabelValue(str) {
if (typeof str !== 'string') {
return str;
}
return escapeString(str).replace(/"/g, '\\"');
}
function escapeString(str) {
return str.replace(/\\/g, '\\\\').replace(/\n/g, '\\n');
}
function standardizeCounterName(name) {
return name.replace(/_total$/, '');
}
module.exports = Registry;
module.exports.globalRegistry = new Registry();