From 80f5ea1d355b698019f9260f66e2f97898b0ead6 Mon Sep 17 00:00:00 2001 From: Ivan Tymoshenko Date: Fri, 4 Oct 2024 15:26:05 +0200 Subject: [PATCH 1/2] feat: add support for custom measurments --- README.md | 9 ++- index.js | 17 ++++-- test/inject.test.js | 140 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 159 insertions(+), 7 deletions(-) create mode 100644 test/inject.test.js diff --git a/README.md b/README.md index 827bbc7..3242c0a 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,14 @@ server.listen(0, () => { - __`histogram`__ `` prom-client [histogram options](https://github.com/siimon/prom-client?tab=readme-ov-file#histogram). Use it if you want to customize the histogram. - __`summary`__ `` prom-client [summary options](https://github.com/siimon/prom-client?tab=readme-ov-file#summary). Use it if you want to customize the summary. +Returns: + +- __`histogram`__ `` The histogram metric for measuring request duration. +- __`summary`__ `` The summary metric for measuring request duration. +- __`startTimer({ request, })`__ `` A function that starts a timer for measuring request duration. +The function can be used to add custom measurements that are not collected by diagnostic channel. +- __`endTimer({ request, response, })` `` A function that ends a timer for custom request duration measurement. + ## License Apache-2.0 - diff --git a/index.js b/index.js index cbbf82c..d37eb9d 100644 --- a/index.js +++ b/index.js @@ -64,8 +64,8 @@ module.exports = (registry, config = {}) => { const timers = new WeakMap() - diagnosticChannel.subscribe('http.server.request.start', (event) => { - const { request, server } = event + function startTimer (options) { + const { request, server } = options if (ignoreRoute(request, server)) return @@ -73,10 +73,10 @@ module.exports = (registry, config = {}) => { const histogramTimer = histogram.startTimer() timers.set(request, { summaryTimer, histogramTimer }) - }) + } - diagnosticChannel.subscribe('http.server.response.finish', (event) => { - const { request, response, server } = event + function endTimer (options) { + const { request, response, server } = options if (ignoreRoute(request, server)) return @@ -93,5 +93,10 @@ module.exports = (registry, config = {}) => { if (summaryTimer) summaryTimer(labels) if (histogramTimer) histogramTimer(labels) - }) + } + + diagnosticChannel.subscribe('http.server.request.start', event => startTimer(event)) + diagnosticChannel.subscribe('http.server.response.finish', event => endTimer(event)) + + return { summary, histogram, startTimer, endTimer } } diff --git a/test/inject.test.js b/test/inject.test.js new file mode 100644 index 0000000..9107195 --- /dev/null +++ b/test/inject.test.js @@ -0,0 +1,140 @@ +'use strict' + +const assert = require('node:assert/strict') +const { test } = require('node:test') +const { setTimeout: sleep } = require('node:timers/promises') +const { Registry } = require('prom-client') +const httpMetrics = require('../index.js') +const { calculateEpsilon } = require('./helper.js') + +test('should calculate the http inject request duration histogram', async (t) => { + const registry = new Registry() + const { startTimer, endTimer } = httpMetrics(registry) + + async function inject (url, ms) { + const request = { method: 'GET', url } + const response = { statusCode: 200 } + + startTimer({ request }) + await sleep(ms) + endTimer({ request, response }) + } + + await Promise.all([ + inject('/500ms', 500), + inject('/1s', 1000), + inject('/2s', 2000), + ]) + + const expectedMeasurements = [0.501, 1.001, 2.001] + const expectedEpsilon = 0.05 + + const metrics = await registry.getMetricsAsJSON() + assert.strictEqual(metrics.length, 2) + + const histogramMetric = metrics.find( + (metric) => metric.name === 'http_request_duration_seconds' + ) + assert.strictEqual(histogramMetric.name, 'http_request_duration_seconds') + assert.strictEqual(histogramMetric.type, 'histogram') + assert.strictEqual(histogramMetric.help, 'request duration in seconds histogram for all requests') + assert.strictEqual(histogramMetric.aggregator, 'sum') + + const histogramValues = histogramMetric.values + + { + const histogramCount = histogramValues.find( + ({ metricName }) => metricName === 'http_request_duration_seconds_count' + ) + assert.strictEqual(histogramCount.value, expectedMeasurements.length) + } + + { + const histogramSum = histogramValues.find( + ({ metricName }) => metricName === 'http_request_duration_seconds_sum' + ) + const value = histogramSum.value + const expectedValue = expectedMeasurements.reduce((a, b) => a + b, 0) + const epsilon = calculateEpsilon(value, expectedValue) + assert.ok( + epsilon < expectedEpsilon, + `expected ${expectedValue}, got ${value}, epsilon ${epsilon}` + ) + } + + for (const { metricName, labels, value } of histogramValues) { + assert.strictEqual(labels.method, 'GET') + assert.strictEqual(labels.status_code, 200) + + if (metricName !== 'http_request_duration_seconds_bucket') continue + + const expectedBucketMeasurements = expectedMeasurements.filter((m) => { + let le = labels.le + if (le === '+Inf') le = Infinity + if (le === '-Inf') le = -Infinity + return m < le + }) + + const expectedValue = expectedBucketMeasurements.length + assert.strictEqual( + value, expectedValue, + `le ${labels.le}: expected ${JSON.stringify(expectedBucketMeasurements)}` + ) + } + + const summaryMetric = metrics.find( + (metric) => metric.name === 'http_request_summary_seconds' + ) + assert.strictEqual(summaryMetric.name, 'http_request_summary_seconds') + assert.strictEqual(summaryMetric.type, 'summary') + assert.strictEqual(summaryMetric.help, 'request duration in seconds summary for all requests') + assert.strictEqual(summaryMetric.aggregator, 'sum') + + const summaryValues = summaryMetric.values + + { + const summaryCount = summaryValues.find( + ({ metricName }) => metricName === 'http_request_summary_seconds_count' + ) + assert.strictEqual(summaryCount.value, expectedMeasurements.length) + } + + { + const summarySum = summaryValues.find( + ({ metricName }) => metricName === 'http_request_summary_seconds_sum' + ) + const value = summarySum.value + const expectedValue = expectedMeasurements.reduce((a, b) => a + b, 0) + const epsilon = calculateEpsilon(value, expectedValue) + assert.ok( + epsilon < expectedEpsilon, + `expected ${expectedValue}, got ${value}, epsilon ${epsilon}` + ) + } + + const expectedSummaryValues = { + 0.01: expectedMeasurements[0], + 0.05: expectedMeasurements[0], + 0.5: expectedMeasurements[1], + 0.9: expectedMeasurements[2], + 0.95: expectedMeasurements[2], + 0.99: expectedMeasurements[2], + 0.999: expectedMeasurements[2], + } + + for (const { labels, value } of summaryValues) { + assert.strictEqual(labels.method, 'GET') + assert.strictEqual(labels.status_code, 200) + + const quantile = labels.quantile + if (quantile === undefined) continue + + const expectedValue = expectedSummaryValues[quantile] + const epsilon = calculateEpsilon(value, expectedValue) + + assert.ok( + epsilon < expectedEpsilon, + `expected ${expectedValue}, got ${value}, epsilon ${epsilon}` + ) + } +}) From 217a77bf978e6f6d8d00ff6969e05b72e457db1d Mon Sep 17 00:00:00 2001 From: Ivan Tymoshenko Date: Fri, 4 Oct 2024 15:28:19 +0200 Subject: [PATCH 2/2] fixup! feat: add support for custom measurments --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 3242c0a..02d7777 100644 --- a/README.md +++ b/README.md @@ -54,9 +54,9 @@ Returns: - __`histogram`__ `` The histogram metric for measuring request duration. - __`summary`__ `` The summary metric for measuring request duration. -- __`startTimer({ request, })`__ `` A function that starts a timer for measuring request duration. +- __`startTimer({ request, [server] })`__ `` A function that starts a timer for measuring request duration. The function can be used to add custom measurements that are not collected by diagnostic channel. -- __`endTimer({ request, response, })` `` A function that ends a timer for custom request duration measurement. +- __`endTimer({ request, response, [server] })` `` A function that ends a timer for custom request duration measurement. ## License