diff --git a/libraries/metrics/index.js b/libraries/metrics/index.js index b84994b28c..460e5967de 100644 --- a/libraries/metrics/index.js +++ b/libraries/metrics/index.js @@ -24,11 +24,16 @@ function configure(opts = {}) { } } +let initialized = false + /** * Configure the metrics module and start the default metrics collectors and * profiling agents. */ function initialize(appName, opts = {}) { + if (initialized) { + return + } appName = appName || DEFAULT_APP_NAME if (tracing.tracingEnabled()) { tracing.initialize(appName) @@ -72,6 +77,7 @@ function initialize(appName, opts = {}) { } inc('process_startup') + initialized = true } function registerDestructor(func) { @@ -84,9 +90,16 @@ function injectMetricsRoute(app) { ExpressCompression({ level: parseInt(process.env.METRICS_COMPRESSION_LEVEL || '1', 10), }), - function (req, res) { + function (req, res, next) { res.set('Content-Type', promWrapper.registry.contentType) - res.end(promWrapper.registry.metrics()) + promWrapper.registry + .metrics() + .then(metrics => { + res.end(metrics) + }) + .catch(err => { + next(err) + }) } ) } @@ -103,70 +116,70 @@ function set(key, value, sampleRate = 1) { console.log('counts are not currently supported') } -function inc(key, sampleRate = 1, opts = {}) { +function inc(key, sampleRate = 1, labels = {}) { if (arguments.length === 2 && typeof sampleRate === 'object') { - opts = sampleRate + labels = sampleRate } key = buildPromKey(key) - promWrapper.metric('counter', key).inc(opts) + promWrapper.metric('counter', key, labels).inc(labels) if (process.env.DEBUG_METRICS) { - console.log('doing inc', key, opts) + console.log('doing inc', key, labels) } } -function count(key, count, sampleRate = 1, opts = {}) { +function count(key, count, sampleRate = 1, labels = {}) { if (arguments.length === 3 && typeof sampleRate === 'object') { - opts = sampleRate + labels = sampleRate } key = buildPromKey(key) - promWrapper.metric('counter', key).inc(opts, count) + promWrapper.metric('counter', key, labels).inc(labels, count) if (process.env.DEBUG_METRICS) { - console.log('doing count/inc', key, opts) + console.log('doing count/inc', key, labels) } } -function summary(key, value, opts = {}) { +function summary(key, value, labels = {}) { key = buildPromKey(key) - promWrapper.metric('summary', key).observe(opts, value) + promWrapper.metric('summary', key, labels).observe(labels, value) if (process.env.DEBUG_METRICS) { - console.log('doing summary', key, value, opts) + console.log('doing summary', key, value, labels) } } -function timing(key, timeSpan, sampleRate = 1, opts = {}) { +function timing(key, timeSpan, sampleRate = 1, labels = {}) { if (arguments.length === 3 && typeof sampleRate === 'object') { - opts = sampleRate + labels = sampleRate } key = buildPromKey('timer_' + key) - promWrapper.metric('summary', key).observe(opts, timeSpan) + promWrapper.metric('summary', key, labels).observe(labels, timeSpan) if (process.env.DEBUG_METRICS) { - console.log('doing timing', key, opts) + console.log('doing timing', key, labels) } } -function histogram(key, value, buckets, opts = {}) { +function histogram(key, value, buckets, labels = {}) { key = buildPromKey('histogram_' + key) - promWrapper.metric('histogram', key, buckets).observe(opts, value) + promWrapper.metric('histogram', key, labels, buckets).observe(labels, value) if (process.env.DEBUG_METRICS) { - console.log('doing histogram', key, buckets, opts) + console.log('doing histogram', key, buckets, labels) } } class Timer { - constructor(key, sampleRate = 1, opts = {}, buckets) { + constructor(key, sampleRate = 1, labels = {}, buckets) { if (typeof sampleRate === 'object') { - // called with (key, opts, buckets) + // called with (key, labels, buckets) if (arguments.length === 3) { - buckets = opts - opts = sampleRate + buckets = labels + labels = sampleRate } - // called with (key, opts) + // called with (key, labels) if (arguments.length === 2) { - opts = sampleRate + labels = sampleRate } sampleRate = 1 // default value to pass to timing function @@ -176,40 +189,37 @@ class Timer { key = buildPromKey(key) this.key = key this.sampleRate = sampleRate - this.opts = opts + this.labels = labels this.buckets = buckets } done() { const timeSpan = new Date() - this.start if (this.buckets) { - histogram(this.key, timeSpan, this.buckets, this.opts) + histogram(this.key, timeSpan, this.buckets, this.labels) } else { - timing(this.key, timeSpan, this.sampleRate, this.opts) + timing(this.key, timeSpan, this.sampleRate, this.labels) } return timeSpan } } -function gauge(key, value, sampleRate = 1, opts = {}) { +function gauge(key, value, sampleRate = 1, labels = {}) { if (arguments.length === 3 && typeof sampleRate === 'object') { - opts = sampleRate + labels = sampleRate } key = buildPromKey(key) - promWrapper - .metric('gauge', key) - .set({ status: opts.status }, sanitizeValue(value)) + promWrapper.metric('gauge', key, labels).set(labels, sanitizeValue(value)) if (process.env.DEBUG_METRICS) { - console.log('doing gauge', key, opts) + console.log('doing gauge', key, labels) } } -function globalGauge(key, value, sampleRate = 1, opts = {}) { +function globalGauge(key, value, sampleRate = 1, labels = {}) { key = buildPromKey(key) - promWrapper - .metric('gauge', key) - .set({ host: 'global', status: opts.status }, sanitizeValue(value)) + labels = { host: 'global', ...labels } + promWrapper.metric('gauge', key, labels).set(labels, sanitizeValue(value)) } function close() { diff --git a/libraries/metrics/package.json b/libraries/metrics/package.json index 0dbc574a08..0eb6b07df4 100644 --- a/libraries/metrics/package.json +++ b/libraries/metrics/package.json @@ -20,7 +20,7 @@ "@opentelemetry/sdk-node": "^0.28.0", "@opentelemetry/semantic-conventions": "^1.2.0", "compression": "^1.7.4", - "prom-client": "^11.1.3", + "prom-client": "^14.1.1", "yn": "^3.1.1" }, "devDependencies": { diff --git a/libraries/metrics/prom_wrapper.js b/libraries/metrics/prom_wrapper.js index eccea17eb9..88f83748f0 100644 --- a/libraries/metrics/prom_wrapper.js +++ b/libraries/metrics/prom_wrapper.js @@ -1,16 +1,10 @@ -/* - * decaffeinate suggestions: - * DS101: Remove unnecessary use of Array.from - * DS102: Remove unnecessary code created because of implicit returns - * DS205: Consider reworking code to avoid use of IIFEs - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md - */ +const logger = require('@overleaf/logger') const prom = require('prom-client') const registry = require('prom-client').register const metrics = new Map() -const optsKey = function (opts) { - let keys = Object.keys(opts) +const labelsKey = function (labels) { + let keys = Object.keys(labels) if (keys.length === 0) { return '' } @@ -18,31 +12,20 @@ const optsKey = function (opts) { keys = keys.sort() let hash = '' - for (const key of Array.from(keys)) { + for (const key of keys) { if (hash.length) { hash += ',' } - hash += `${key}:${opts[key]}` + hash += `${key}:${labels[key]}` } return hash } -const extendOpts = function (opts, labelNames) { - // Make a clone in order to be able to re-use opts for other kinds of metrics. - opts = Object.assign({}, opts) - for (const label of Array.from(labelNames)) { - if (!opts[label]) { - opts[label] = '' - } - } - return opts -} - -const optsAsArgs = function (opts, labelNames) { +const labelsAsArgs = function (labels, labelNames) { const args = [] - for (const label of Array.from(labelNames)) { - args.push(opts[label] || '') + for (const label of labelNames) { + args.push(labels[label] || '') } return args } @@ -51,74 +34,68 @@ const PromWrapper = { ttlInMinutes: 0, registry, - metric(type, name, buckets) { - return metrics.get(name) || new MetricWrapper(type, name, buckets) + metric(type, name, labels, buckets) { + return metrics.get(name) || new MetricWrapper(type, name, labels, buckets) }, collectDefaultMetrics: prom.collectDefaultMetrics, } class MetricWrapper { - constructor(type, name, buckets) { + constructor(type, name, labels, buckets) { metrics.set(name, this) this.name = name this.instances = new Map() this.lastAccess = new Date() - this.metric = (() => { - switch (type) { - case 'counter': - return new prom.Counter({ - name, - help: name, - labelNames: ['status', 'method', 'path'], - }) - case 'histogram': - return new prom.Histogram({ - name, - help: name, - labelNames: [ - 'path', - 'status_code', - 'method', - 'collection', - 'query', - ], - buckets, - }) - case 'summary': - return new prom.Summary({ - name, - help: name, - maxAgeSeconds: 60, - ageBuckets: 10, - labelNames: [ - 'path', - 'status_code', - 'method', - 'collection', - 'query', - ], - }) - case 'gauge': - return new prom.Gauge({ - name, - help: name, - labelNames: ['host', 'status'], - }) - } - })() + + const labelNames = labels ? Object.keys(labels) : [] + switch (type) { + case 'counter': + this.metric = new prom.Counter({ + name, + help: name, + labelNames, + }) + break + case 'histogram': + this.metric = new prom.Histogram({ + name, + help: name, + labelNames, + buckets, + }) + break + case 'summary': + this.metric = new prom.Summary({ + name, + help: name, + maxAgeSeconds: 60, + ageBuckets: 10, + labelNames, + }) + break + case 'gauge': + this.metric = new prom.Gauge({ + name, + help: name, + labelNames, + }) + break + default: + throw new Error(`Unknown metric type: ${type}`) + } } - inc(opts, value) { - return this._execMethod('inc', opts, value) + inc(labels, value) { + this._execMethod('inc', labels, value) } - observe(opts, value) { - return this._execMethod('observe', opts, value) + observe(labels, value) { + this._execMethod('observe', labels, value) } - set(opts, value) { - return this._execMethod('set', opts, value) + set(labels, value) { + this._execMethod('set', labels, value) } sweep() { @@ -130,12 +107,12 @@ class MetricWrapper { console.log( 'Sweeping stale metric instance', this.name, - { opts: instance.opts }, + { labels: instance.labels }, key ) } - return this.metric.remove( - ...Array.from(optsAsArgs(instance.opts, this.metric.labelNames) || []) + this.metric.remove( + ...labelsAsArgs(instance.labels, this.metric.labelNames) ) } }) @@ -146,18 +123,24 @@ class MetricWrapper { console.log('Sweeping stale metric', this.name, thresh, this.lastAccess) } metrics.delete(this.name) - return registry.removeSingleMetric(this.name) + registry.removeSingleMetric(this.name) } } - _execMethod(method, opts, value) { - opts = extendOpts(opts, this.metric.labelNames) - const key = optsKey(opts) + _execMethod(method, labels, value) { + const key = labelsKey(labels) if (key !== '') { - this.instances.set(key, { time: new Date(), opts }) + this.instances.set(key, { time: new Date(), labels }) } this.lastAccess = new Date() - return this.metric[method](opts, value) + try { + this.metric[method](labels, value) + } catch (err) { + logger.warn( + { err, metric: this.metric.name, labels }, + 'failed to record metric' + ) + } } } @@ -182,8 +165,8 @@ PromWrapper.setupSweeping = function () { // eslint-disable-next-line no-console console.log('Sweeping metrics') } - return metrics.forEach((metric, key) => { - return metric.sweep() + metrics.forEach((metric, key) => { + metric.sweep() }) }, 60000) diff --git a/package-lock.json b/package-lock.json index 7552268205..d2c3c79869 100644 --- a/package-lock.json +++ b/package-lock.json @@ -136,7 +136,7 @@ "@opentelemetry/sdk-node": "^0.28.0", "@opentelemetry/semantic-conventions": "^1.2.0", "compression": "^1.7.4", - "prom-client": "^11.1.3", + "prom-client": "^14.1.1", "yn": "^3.1.1" }, "devDependencies": { @@ -25779,14 +25779,14 @@ "integrity": "sha512-du4wfLyj4yCZq1VupnVSZmRsPJsNuxoDQFdCFHLaYiEbFBD7QE0a+I4D7hOxrVnh78QE/YipFAj9lXHiXocV+Q==" }, "node_modules/prom-client": { - "version": "11.5.3", - "resolved": "https://registry.npmjs.org/prom-client/-/prom-client-11.5.3.tgz", - "integrity": "sha512-iz22FmTbtkyL2vt0MdDFY+kWof+S9UB/NACxSn2aJcewtw+EERsen0urSkZ2WrHseNdydsvcxCTAnPcSMZZv4Q==", + "version": "14.1.1", + "resolved": "https://registry.npmjs.org/prom-client/-/prom-client-14.1.1.tgz", + "integrity": "sha512-hFU32q7UZQ59bVJQGUtm3I2PrJ3gWvoCkilX9sF165ks1qflhugVCeK+S1JjJYHvyt3o5kj68+q3bchormjnzw==", "dependencies": { "tdigest": "^0.1.1" }, "engines": { - "node": ">=6.1" + "node": ">=10" } }, "node_modules/promise": { @@ -43170,7 +43170,7 @@ "chai": "^4.3.6", "compression": "^1.7.4", "mocha": "^10.2.0", - "prom-client": "^11.1.3", + "prom-client": "^14.1.1", "sandboxed-module": "^2.0.4", "sinon": "^9.2.4", "yn": "^3.1.1" @@ -60770,9 +60770,9 @@ "integrity": "sha512-du4wfLyj4yCZq1VupnVSZmRsPJsNuxoDQFdCFHLaYiEbFBD7QE0a+I4D7hOxrVnh78QE/YipFAj9lXHiXocV+Q==" }, "prom-client": { - "version": "11.5.3", - "resolved": "https://registry.npmjs.org/prom-client/-/prom-client-11.5.3.tgz", - "integrity": "sha512-iz22FmTbtkyL2vt0MdDFY+kWof+S9UB/NACxSn2aJcewtw+EERsen0urSkZ2WrHseNdydsvcxCTAnPcSMZZv4Q==", + "version": "14.1.1", + "resolved": "https://registry.npmjs.org/prom-client/-/prom-client-14.1.1.tgz", + "integrity": "sha512-hFU32q7UZQ59bVJQGUtm3I2PrJ3gWvoCkilX9sF165ks1qflhugVCeK+S1JjJYHvyt3o5kj68+q3bchormjnzw==", "requires": { "tdigest": "^0.1.1" }