Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add support for custom measurments #3

Merged
merged 2 commits into from
Oct 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,14 @@ server.listen(0, () => {
- __`histogram`__ `<object>` 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`__ `<object>` 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`__ `<Histogram>` The histogram metric for measuring request duration.
- __`summary`__ `<Summary>` The summary metric for measuring request duration.
- __`startTimer({ request, [server] })`__ `<function>` 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, [server] })` `<function>` A function that ends a timer for custom request duration measurement.

## License

Apache-2.0

17 changes: 11 additions & 6 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,19 +64,19 @@ 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

const summaryTimer = summary.startTimer()
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

Expand All @@ -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 }
}
140 changes: 140 additions & 0 deletions test/inject.test.js
Original file line number Diff line number Diff line change
@@ -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}`
)
}
})
Loading