diff --git a/src/index.js b/src/index.js index 1d9a524..6e4e286 100644 --- a/src/index.js +++ b/src/index.js @@ -4,6 +4,7 @@ import PullTransport from './transport/PullTransport'; import PushTransport from './transport/PushTransport'; import Metric from './metric/Metric'; import GaugeMetric from './metric/GaugeMetric'; +import HistogramMetric from './metric/HistogramMetric'; export { UMetrics, @@ -12,4 +13,5 @@ export { PullTransport, GaugeMetric, Metric, + HistogramMetric, }; diff --git a/src/metric/HistogramMetric.js b/src/metric/HistogramMetric.js new file mode 100644 index 0000000..0e826cb --- /dev/null +++ b/src/metric/HistogramMetric.js @@ -0,0 +1,60 @@ +import promClient from 'prom-client'; +import Metric from './Metric'; + +class HistogramMetric extends Metric { + constructor(name, buckets = [], options = {}) { + super(name, options); + + this.promMetric = new promClient.Histogram({ + name: this.name, + help: options.help || `${this.name}_help`, + labelNames: Object.keys(this._labels), + buckets, + }); + } + + /** + * @param {string} action + * @param {number} value + * @param {Object.} [_labels] + * @return {HistogramMetric} + * @private + */ + _proxyCall(action, value, _labels) { + const labels = { ...this._labels, ..._labels }; + if (!labels) { + this.promMetric[action](value); + return this; + } + + this.promMetric[action](labels, value); + return this; + } + + /** + * @param {number} value + * @param {Object.} [labels] + * @returns {HistogramMetric} + */ + observe(value, labels) { + return this._proxyCall('observe', value, labels); + } + + /** + * Start a timer where the value in seconds will observed + * @param labels Object with label keys and values + * @return Function to invoke when timer should be stopped + */ + startTimer(labels) { + return this.promMetric.startTimer(labels); + } + + /** + * Reset histogram values + */ + reset() { + this.promMetric.reset(); + } +} + +export default HistogramMetric; diff --git a/src/metric/HistogramMetric.server.test.js b/src/metric/HistogramMetric.server.test.js new file mode 100644 index 0000000..42f3a1f --- /dev/null +++ b/src/metric/HistogramMetric.server.test.js @@ -0,0 +1,172 @@ +import { expect } from 'chai'; +import promClient from 'prom-client'; +import HistogramMetric from './HistogramMetric'; + +/** + * @param {string} metricName + * @return {number} + */ +const getMetricValues = metricName => + promClient.register.getSingleMetric(metricName).get().values; + +/** + * @param {string} metricName + * @param {string} labelName + * @return {string|number} + */ +const getLabelValue = (metricName, labelName) => + promClient.register.getSingleMetric(metricName).get().values[0].labels[ + labelName + ]; + +describe('HistogramMetric', () => { + // Чтобы не ебалось при --watch + afterEach(() => { + promClient.register.clear(); + }); + + describe('observe', () => { + it('Can observe value without labels', () => { + const metricName = 'test'; + + const metric = new HistogramMetric(metricName, [5, 100, 1000]); + + metric.observe(5); + metric.observe(50); + metric.observe(100); + metric.observe(1001); + + const metricValues = getMetricValues(metricName); + + expect(metricValues[0].labels.le).to.be.equal(5); + expect(metricValues[0].value).to.be.equal(1); + expect(metricValues[0].metricName).to.be.equal(`${metricName}_bucket`); + + expect(metricValues[1].labels.le).to.be.equal(100); + expect(metricValues[1].value).to.be.equal(3); + + expect(metricValues[2].labels.le).to.be.equal(1000); + expect(metricValues[2].value).to.be.equal(3); + + expect(metricValues[3].labels.le).to.be.equal('+Inf'); + expect(metricValues[3].value).to.be.equal(4); + + expect(metricValues[4].value).to.be.equal(1156); + expect(metricValues[4].metricName).to.be.equal(`${metricName}_sum`); + }); + + it('Can observe value with labels', () => { + const metricName = 'test'; + const labelName = 'testLabel'; + + const metric = new HistogramMetric(metricName, [100], { + labels: { testLabel: null }, + }); + + metric.observe(50, { [labelName]: 'testLabel' }); + metric.observe(150, { [labelName]: 'testLabel' }); + + const labelValue = getLabelValue(metricName, labelName); + expect(labelValue).to.be.equal(labelName); + + const metricValues = getMetricValues(metricName); + expect(metricValues[0].labels.le).to.be.equal(100); + expect(metricValues[0].value).to.be.equal(1); + expect(metricValues[0].metricName).to.be.equal(`${metricName}_bucket`); + + expect(metricValues[1].labels.le).to.be.equal('+Inf'); + expect(metricValues[1].value).to.be.equal(2); + + expect(metricValues[2].value).to.be.equal(200); + expect(metricValues[2].metricName).to.be.equal(`${metricName}_sum`); + }); + + it('Can observe value without buckets', () => { + const metricName = 'test'; + + const metric = new HistogramMetric(metricName); + + metric.observe(50); + metric.observe(150); + + const metricValues = getMetricValues(metricName); + + expect(metricValues[0].labels.le).to.be.equal('+Inf'); + expect(metricValues[0].value).to.be.equal(2); + + expect(metricValues[1].value).to.be.equal(200); + expect(metricValues[1].metricName).to.be.equal(`${metricName}_sum`); + }); + }); + + describe('startTimer', () => { + it('returns function and starts timer', async () => { + const metricName = 'test'; + + const metric = new HistogramMetric(metricName, [0.005, 0.1, 1]); + + let end = metric.startTimer(); + await new Promise(resolve => { + setTimeout(() => { + end(); + resolve(); + }, 2); + }); + end = metric.startTimer(); + await new Promise(resolve => { + setTimeout(() => { + end(); + resolve(); + }, 200); + }); + + const metricValues = getMetricValues(metricName); + + expect(metricValues[0].labels.le).to.be.equal(0.005); + expect(metricValues[0].value).to.be.equal(1); + expect(metricValues[0].metricName).to.be.equal(`${metricName}_bucket`); + + expect(metricValues[1].labels.le).to.be.equal(0.1); + expect(metricValues[1].value).to.be.equal(1); + + expect(metricValues[2].labels.le).to.be.equal(1); + expect(metricValues[2].value).to.be.equal(2); + + expect(metricValues[3].labels.le).to.be.equal('+Inf'); + expect(metricValues[3].value).to.be.equal(2); + }); + }); + + describe('reset', () => { + it('should reset metrics values', async () => { + const metricName = 'test'; + + const metric = new HistogramMetric(metricName, [5, 100, 1000]); + + metric.observe(5); + metric.observe(101); + + metric.reset(); + + metric.observe(5); + + const metricValues = getMetricValues(metricName); + + expect(metricValues[0].labels.le).to.be.equal(5); + expect(metricValues[0].value).to.be.equal(1); + expect(metricValues[0].metricName).to.be.equal(`${metricName}_bucket`); + + expect(metricValues[1].labels.le).to.be.equal(100); + expect(metricValues[1].value).to.be.equal(1); + + expect(metricValues[2].labels.le).to.be.equal(1000); + expect(metricValues[2].value).to.be.equal(1); + + expect(metricValues[3].labels.le).to.be.equal('+Inf'); + expect(metricValues[3].value).to.be.equal(1); + + expect(metricValues[4].value).to.be.equal(5); + expect(metricValues[4].metricName).to.be.equal(`${metricName}_sum`); + }); + }); +});