From 2af04537769ed8665011d36d1d38b9e9fafedefb Mon Sep 17 00:00:00 2001 From: Dan Shappir Date: Thu, 9 Mar 2023 13:39:40 +0200 Subject: [PATCH] perf: improve performance of getMetricAsString in next (#548) Co-authored-by: Dan Shappir --- lib/registry.js | 128 ++++++++++++++++++------------------------------ 1 file changed, 49 insertions(+), 79 deletions(-) diff --git a/lib/registry.js b/lib/registry.js index 2b151809..127c659a 100644 --- a/lib/registry.js +++ b/lib/registry.js @@ -2,22 +2,6 @@ const { getValueAsString } = require('./util'); -function escapeString(str) { - return str.replace(/\n/g, '\\n').replace(/\\(?!n)/g, '\\\\'); -} -function escapeLabelValue(str) { - if (typeof str !== 'string') { - return str; - } - return escapeString(str).replace(/"/g, '\\"'); -} -function standardizeCounterName(name) { - if (name.endsWith('_total')) { - return name.replace('_total', ''); - } - return name; -} - class Registry { static get PROMETHEUS_CONTENT_TYPE() { return 'text/plain; version=0.0.4; charset=utf-8'; @@ -44,24 +28,19 @@ class Registry { return Object.values(this._metrics); } - getLabelSetAsString(metric) { - const defaultLabelNames = Object.keys(this._defaultLabels); - let values = ''; - - for (const val of metric.values || []) { - val.labels = val.labels || {}; + async getMetricsAsString(metrics) { + const metric = await metrics.get(); - if (defaultLabelNames.length > 0) { - // Make a copy before mutating - val.labels = Object.assign({}, val.labels); + const name = escapeString(metric.name); + const help = `# HELP ${name} ${escapeString(metric.help)}`; + const type = `# TYPE ${name} ${metric.type}`; + const values = [help, type]; - for (const labelName of defaultLabelNames) { - val.labels[labelName] = - val.labels[labelName] || this._defaultLabels[labelName]; - } - } + const defaultLabels = + Object.keys(this._defaultLabels).length > 0 ? this._defaultLabels : null; - let metricName = val.metricName || metric.name; + for (const val of metric.values || []) { + let { metricName = name, labels = {} } = val; if ( this.contentType === Registry.OPENMETRICS_CONTENT_TYPE && metric.type === 'counter' @@ -69,59 +48,31 @@ class Registry { metricName = `${metricName}_total`; } - const keys = Object.keys(val.labels); - const size = keys.length; - if (size > 0) { - let labels = ''; - let i = 0; - for (; i < size - 1; i++) { - labels += `${keys[i]}="${escapeLabelValue(val.labels[keys[i]])}",`; - } - labels += `${keys[i]}="${escapeLabelValue(val.labels[keys[i]])}"`; - metricName += `{${labels}}`; + if (defaultLabels) { + labels = { ...labels, ...defaultLabels, ...labels }; } - values += `${metricName} ${getValueAsString(val.value)}`; - if ( - val.exemplar && - this.contentType === Registry.OPENMETRICS_CONTENT_TYPE - ) { - const exemplarKeys = Object.keys(val.exemplar.labelSet); - const exemplarSize = exemplarKeys.length; - if (exemplarSize > 0) { - let labels = ''; - let i = 0; - for (; i < exemplarSize - 1; i++) { - labels += `${exemplarKeys[i]}="${escapeLabelValue( - val.exemplar.labelSet[exemplarKeys[i]], - )}",`; - } - labels += `${exemplarKeys[i]}="${escapeLabelValue( - val.exemplar.labelSet[exemplarKeys[i]], - )}"`; - values += ` # {${labels}} ${getValueAsString(val.exemplar.value)} ${ - val.exemplar.timestamp - }`; - } else { - values += ` # {} ${getValueAsString(val.exemplar.value)} ${ - val.exemplar.timestamp - }`; - } - } - values += '\n'; - } - return values; - } + const formattedLabels = formatLabels(labels); + const labelsString = formattedLabels.length + ? `{${formattedLabels.join(',')}}` + : ''; - async getMetricsAsString(metrics) { - const metric = await metrics.get(); + values.push( + `${metricName}${labelsString} ${getValueAsString(val.value)}`, + ); - const name = escapeString(metric.name); - const help = `# HELP ${name} ${escapeString(metric.help)}`; - const type = `# TYPE ${name} ${metric.type}`; - const values = this.getLabelSetAsString(metric); + const { exemplar } = val; + if (exemplar && this.contentType === Registry.OPENMETRICS_CONTENT_TYPE) { + const formattedExemplars = formatLabels(exemplar.labelSet); + values.push( + ` # {${formattedExemplars.join(',')}} ${getValueAsString( + exemplar.value, + )} ${exemplar.timestamp}`, + ); + } + } - return `${help}\n${type}\n${values}`.trim(); + return values.join('\n'); } async metrics() { @@ -250,5 +201,24 @@ class Registry { } } +function formatLabels(labels) { + return Object.entries(labels).map( + ([n, v]) => `${n}="${escapeLabelValue(v)}"`, + ); +} + +function escapeLabelValue(str) { + if (typeof str !== 'string') { + return str; + } + return escapeString(str).replace(/"/g, '\\"'); +} +function escapeString(str) { + return str.replace(/\n/g, '\\n').replace(/\\(?!n)/g, '\\\\'); +} +function standardizeCounterName(name) { + return name.replace(/_total$/, ''); +} + module.exports = Registry; module.exports.globalRegistry = new Registry();