first commit
This commit is contained in:
343
libraries/metrics/test/acceptance/metrics_tests.js
Normal file
343
libraries/metrics/test/acceptance/metrics_tests.js
Normal file
@@ -0,0 +1,343 @@
|
||||
const { promisify } = require('node:util')
|
||||
const os = require('node:os')
|
||||
const http = require('node:http')
|
||||
const { expect } = require('chai')
|
||||
const Metrics = require('../..')
|
||||
|
||||
const HOSTNAME = os.hostname()
|
||||
const APP_NAME = 'test-app'
|
||||
const sleep = promisify(setTimeout)
|
||||
|
||||
describe('Metrics module', function () {
|
||||
before(function () {
|
||||
process.env.METRICS_APP_NAME = 'test-app'
|
||||
require('../../initialize')
|
||||
})
|
||||
|
||||
describe('at startup', function () {
|
||||
it('increments the process_startup counter', async function () {
|
||||
await expectMetricValue('process_startup', 1)
|
||||
})
|
||||
|
||||
it('collects default metrics', async function () {
|
||||
const metric = await getMetric('process_cpu_user_seconds_total')
|
||||
expect(metric).to.exist
|
||||
})
|
||||
})
|
||||
|
||||
describe('inc()', function () {
|
||||
it('increments counts by 1', async function () {
|
||||
Metrics.inc('duck_count')
|
||||
await expectMetricValue('duck_count', 1)
|
||||
Metrics.inc('duck_count')
|
||||
Metrics.inc('duck_count')
|
||||
await expectMetricValue('duck_count', 3)
|
||||
})
|
||||
|
||||
it('escapes special characters in the key', async function () {
|
||||
Metrics.inc('show.me the $!!')
|
||||
await expectMetricValue('show_me_the____', 1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('count()', function () {
|
||||
it('increments counts by the given count', async function () {
|
||||
Metrics.count('rabbit_count', 5)
|
||||
await expectMetricValue('rabbit_count', 5)
|
||||
Metrics.count('rabbit_count', 6)
|
||||
Metrics.count('rabbit_count', 7)
|
||||
await expectMetricValue('rabbit_count', 18)
|
||||
})
|
||||
})
|
||||
|
||||
describe('summary()', function () {
|
||||
it('collects observations', async function () {
|
||||
Metrics.summary('oven_temp', 200)
|
||||
Metrics.summary('oven_temp', 300)
|
||||
Metrics.summary('oven_temp', 450)
|
||||
const sum = await getSummarySum('oven_temp')
|
||||
expect(sum).to.equal(950)
|
||||
})
|
||||
})
|
||||
|
||||
describe('timing()', function () {
|
||||
it('collects timings', async function () {
|
||||
Metrics.timing('sprint_100m', 10)
|
||||
Metrics.timing('sprint_100m', 20)
|
||||
Metrics.timing('sprint_100m', 30)
|
||||
const sum = await getSummarySum('timer_sprint_100m')
|
||||
expect(sum).to.equal(60)
|
||||
})
|
||||
})
|
||||
|
||||
describe('histogram()', function () {
|
||||
it('collects in buckets', async function () {
|
||||
const buckets = [10, 100, 1000]
|
||||
Metrics.histogram('distance', 10, buckets)
|
||||
Metrics.histogram('distance', 20, buckets)
|
||||
Metrics.histogram('distance', 100, buckets)
|
||||
Metrics.histogram('distance', 200, buckets)
|
||||
Metrics.histogram('distance', 1000, buckets)
|
||||
Metrics.histogram('distance', 2000, buckets)
|
||||
const sum = await getSummarySum('histogram_distance')
|
||||
expect(sum).to.equal(3330)
|
||||
await checkHistogramValues('histogram_distance', {
|
||||
10: 1,
|
||||
100: 3,
|
||||
1000: 5,
|
||||
'+Inf': 6,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Timer', function () {
|
||||
beforeEach('collect timings', async function () {
|
||||
const buckets = [10, 100, 1000]
|
||||
for (const duration of [1, 1, 1, 15, 15, 15, 105, 105, 105]) {
|
||||
const withBuckets = new Metrics.Timer(
|
||||
'height',
|
||||
1,
|
||||
{ label_1: 'a' },
|
||||
buckets
|
||||
)
|
||||
const withOutBuckets = new Metrics.Timer('depth', 1, { label_2: 'b' })
|
||||
await sleep(duration)
|
||||
withBuckets.done()
|
||||
withOutBuckets.done({ label_3: 'c' })
|
||||
}
|
||||
})
|
||||
|
||||
it('with buckets', async function () {
|
||||
await checkHistogramValues('histogram_height', {
|
||||
10: 3,
|
||||
100: 6,
|
||||
1000: 9,
|
||||
'+Inf': 9,
|
||||
})
|
||||
const labelNames = await getMetric('histogram_height').labelNames
|
||||
expect(labelNames).to.deep.equal(['label_1'])
|
||||
})
|
||||
|
||||
it('without buckets', async function () {
|
||||
await checkSummaryValues('timer_depth', {
|
||||
0.01: 1,
|
||||
0.05: 1,
|
||||
0.5: 15,
|
||||
0.9: 105,
|
||||
0.95: 105,
|
||||
0.99: 105,
|
||||
0.999: 105,
|
||||
})
|
||||
const labelNames = await getMetric('timer_depth').labelNames
|
||||
expect(labelNames).to.deep.equal(['label_2', 'label_3'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('gauge()', function () {
|
||||
it('records values', async function () {
|
||||
Metrics.gauge('water_level', 1.5)
|
||||
await expectMetricValue('water_level', 1.5)
|
||||
Metrics.gauge('water_level', 4.2)
|
||||
await expectMetricValue('water_level', 4.2)
|
||||
})
|
||||
})
|
||||
|
||||
describe('globalGauge()', function () {
|
||||
it('records values without a host label', async function () {
|
||||
Metrics.globalGauge('tire_pressure', 99.99)
|
||||
const { value, labels } = await getMetricValue('tire_pressure')
|
||||
expect(value).to.equal(99.99)
|
||||
expect(labels.host).to.equal('global')
|
||||
expect(labels.app).to.equal(APP_NAME)
|
||||
})
|
||||
})
|
||||
|
||||
describe('open_sockets', function () {
|
||||
const keyServer1 = 'open_connections_http_127_42_42_1'
|
||||
const keyServer2 = 'open_connections_http_127_42_42_2'
|
||||
|
||||
let finish1, finish2, emitResponse1, emitResponse2
|
||||
function resetEmitResponse1() {
|
||||
emitResponse1 = new Promise(resolve => (finish1 = resolve))
|
||||
}
|
||||
resetEmitResponse1()
|
||||
function resetEmitResponse2() {
|
||||
emitResponse2 = new Promise(resolve => (finish2 = resolve))
|
||||
}
|
||||
resetEmitResponse2()
|
||||
|
||||
let server1, server2
|
||||
before(function setupServer1(done) {
|
||||
server1 = http.createServer((req, res) => {
|
||||
res.write('...')
|
||||
emitResponse1.then(() => res.end())
|
||||
})
|
||||
server1.listen(0, '127.42.42.1', done)
|
||||
})
|
||||
before(function setupServer2(done) {
|
||||
server2 = http.createServer((req, res) => {
|
||||
res.write('...')
|
||||
emitResponse2.then(() => res.end())
|
||||
})
|
||||
server2.listen(0, '127.42.42.2', done)
|
||||
})
|
||||
after(function cleanupPendingRequests() {
|
||||
finish1()
|
||||
finish2()
|
||||
})
|
||||
after(function shutdownServer1(done) {
|
||||
if (server1) server1.close(done)
|
||||
})
|
||||
after(function shutdownServer2(done) {
|
||||
if (server2) server2.close(done)
|
||||
})
|
||||
|
||||
let urlServer1, urlServer2
|
||||
before(function setUrls() {
|
||||
urlServer1 = `http://127.42.42.1:${server1.address().port}/`
|
||||
urlServer2 = `http://127.42.42.2:${server2.address().port}/`
|
||||
})
|
||||
describe('gaugeOpenSockets()', function () {
|
||||
beforeEach(function runGaugeOpenSockets() {
|
||||
Metrics.open_sockets.gaugeOpenSockets(true)
|
||||
})
|
||||
|
||||
describe('without pending connections', function () {
|
||||
it('emits no open_connections', async function () {
|
||||
await expectNoMetricValue(keyServer1)
|
||||
await expectNoMetricValue(keyServer2)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with pending connections for server1', function () {
|
||||
before(function (done) {
|
||||
http.get(urlServer1)
|
||||
http.get(urlServer1)
|
||||
setTimeout(done, 10)
|
||||
})
|
||||
|
||||
it('emits 2 open_connections for server1', async function () {
|
||||
await expectMetricValue(keyServer1, 2)
|
||||
})
|
||||
|
||||
it('emits no open_connections for server2', async function () {
|
||||
await expectNoMetricValue(keyServer2)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with pending connections for server1 and server2', function () {
|
||||
before(function (done) {
|
||||
http.get(urlServer2)
|
||||
http.get(urlServer2)
|
||||
setTimeout(done, 10)
|
||||
})
|
||||
|
||||
it('emits 2 open_connections for server1', async function () {
|
||||
await expectMetricValue(keyServer1, 2)
|
||||
})
|
||||
|
||||
it('emits 2 open_connections for server2', async function () {
|
||||
await expectMetricValue(keyServer2, 2)
|
||||
})
|
||||
})
|
||||
|
||||
describe('when requests finish for server1', function () {
|
||||
before(function (done) {
|
||||
finish1()
|
||||
resetEmitResponse1()
|
||||
http.get(urlServer1)
|
||||
|
||||
setTimeout(done, 10)
|
||||
})
|
||||
|
||||
it('emits 1 open_connections for server1', async function () {
|
||||
await expectMetricValue(keyServer1, 1)
|
||||
})
|
||||
|
||||
it('emits 2 open_connections for server2', async function () {
|
||||
await expectMetricValue(keyServer2, 2)
|
||||
})
|
||||
})
|
||||
|
||||
describe('when all requests complete', function () {
|
||||
before(function (done) {
|
||||
finish1()
|
||||
finish2()
|
||||
|
||||
setTimeout(done, 10)
|
||||
})
|
||||
|
||||
it('emits no open_connections', async function () {
|
||||
await expectNoMetricValue(keyServer1)
|
||||
await expectNoMetricValue(keyServer2)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
function getMetric(key) {
|
||||
return Metrics.register.getSingleMetric(key)
|
||||
}
|
||||
|
||||
async function getSummarySum(key) {
|
||||
const metric = getMetric(key)
|
||||
const item = await metric.get()
|
||||
for (const value of item.values) {
|
||||
if (value.metricName === `${key}_sum`) {
|
||||
return value.value
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
async function checkHistogramValues(key, values) {
|
||||
const metric = getMetric(key)
|
||||
const item = await metric.get()
|
||||
const found = {}
|
||||
for (const value of item.values) {
|
||||
const bucket = value.labels.le
|
||||
if (!bucket) continue
|
||||
found[bucket] = value.value
|
||||
}
|
||||
expect(found).to.deep.equal(values)
|
||||
return null
|
||||
}
|
||||
|
||||
async function checkSummaryValues(key, values) {
|
||||
const metric = getMetric(key)
|
||||
const item = await metric.get()
|
||||
const found = {}
|
||||
for (const value of item.values) {
|
||||
const quantile = value.labels.quantile
|
||||
if (!quantile) continue
|
||||
found[quantile] = value.value
|
||||
}
|
||||
for (const quantile of Object.keys(values)) {
|
||||
expect(found[quantile]).to.be.within(
|
||||
values[quantile] - 5,
|
||||
values[quantile] + 15,
|
||||
`quantile: ${quantile}`
|
||||
)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
async function getMetricValue(key) {
|
||||
const metrics = await Metrics.register.getMetricsAsJSON()
|
||||
const metric = metrics.find(m => m.name === key)
|
||||
return metric.values[0]
|
||||
}
|
||||
|
||||
async function expectMetricValue(key, expectedValue) {
|
||||
const value = await getMetricValue(key)
|
||||
expect(value.value).to.equal(expectedValue)
|
||||
expect(value.labels.host).to.equal(HOSTNAME)
|
||||
expect(value.labels.app).to.equal(APP_NAME)
|
||||
}
|
||||
|
||||
async function expectNoMetricValue(key) {
|
||||
const metric = getMetric(key)
|
||||
if (!metric) return
|
||||
await expectMetricValue(key, 0)
|
||||
}
|
44
libraries/metrics/test/unit/js/event_loop.js
Normal file
44
libraries/metrics/test/unit/js/event_loop.js
Normal file
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* decaffeinate suggestions:
|
||||
* DS102: Remove unnecessary code created because of implicit returns
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
const chai = require('chai')
|
||||
const { expect } = chai
|
||||
const path = require('node:path')
|
||||
const modulePath = path.join(__dirname, '../../../event_loop.js')
|
||||
const SandboxedModule = require('sandboxed-module')
|
||||
const sinon = require('sinon')
|
||||
|
||||
describe('event_loop', function () {
|
||||
before(function () {
|
||||
this.metrics = {
|
||||
timing: sinon.stub(),
|
||||
registerDestructor: sinon.stub(),
|
||||
}
|
||||
this.logger = {
|
||||
warn: sinon.stub(),
|
||||
}
|
||||
return (this.event_loop = SandboxedModule.require(modulePath, {
|
||||
requires: {
|
||||
'./index': this.metrics,
|
||||
},
|
||||
}))
|
||||
})
|
||||
|
||||
describe('with a logger provided', function () {
|
||||
before(function () {
|
||||
return this.event_loop.monitor(this.logger)
|
||||
})
|
||||
|
||||
return it('should register a destructor with metrics', function () {
|
||||
return expect(this.metrics.registerDestructor.called).to.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
return describe('without a logger provided', function () {
|
||||
return it('should throw an exception', function () {
|
||||
return expect(this.event_loop.monitor).to.throw('logger is undefined')
|
||||
})
|
||||
})
|
||||
})
|
171
libraries/metrics/test/unit/js/http.js
Normal file
171
libraries/metrics/test/unit/js/http.js
Normal file
@@ -0,0 +1,171 @@
|
||||
const Path = require('node:path')
|
||||
const SandboxedModule = require('sandboxed-module')
|
||||
const sinon = require('sinon')
|
||||
|
||||
const MODULE_PATH = Path.join(__dirname, '../../../http.js')
|
||||
|
||||
describe('http.monitor', function () {
|
||||
beforeEach(function () {
|
||||
this.req = {
|
||||
method: 'POST',
|
||||
url: '/project/1234/cleanup',
|
||||
headers: {
|
||||
'content-length': '123',
|
||||
},
|
||||
route: {
|
||||
path: '/project/:id/cleanup',
|
||||
},
|
||||
}
|
||||
this.originalResponseEnd = sinon.stub()
|
||||
this.res = {
|
||||
end: this.originalResponseEnd,
|
||||
}
|
||||
this.data = 'data'
|
||||
this.logger = {
|
||||
debug: sinon.stub(),
|
||||
info: sinon.stub(),
|
||||
warn: sinon.stub(),
|
||||
}
|
||||
this.Metrics = {
|
||||
timing: sinon.stub(),
|
||||
summary: sinon.stub(),
|
||||
}
|
||||
this.clock = sinon.useFakeTimers()
|
||||
|
||||
this.http = SandboxedModule.require(MODULE_PATH, {
|
||||
requires: {
|
||||
'./index': this.Metrics,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(function () {
|
||||
this.clock.restore()
|
||||
})
|
||||
|
||||
describe('with the default options', function () {
|
||||
beforeEach('set up the monitor', function (done) {
|
||||
this.http.monitor(this.logger)(this.req, this.res, done)
|
||||
})
|
||||
|
||||
describe('after a simple request', function () {
|
||||
endRequest()
|
||||
expectOriginalEndCalled()
|
||||
expectMetrics()
|
||||
|
||||
it('logs the request at the DEBUG level', function () {
|
||||
sinon.assert.calledWith(
|
||||
this.logger.debug,
|
||||
{ req: this.req, res: this.res, responseTimeMs: 500 },
|
||||
'%s %s',
|
||||
this.req.method,
|
||||
this.req.url
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('when logging is disabled', function () {
|
||||
beforeEach('disable logging', function () {
|
||||
this.req.logger.disable()
|
||||
})
|
||||
|
||||
endRequest()
|
||||
expectOriginalEndCalled()
|
||||
expectMetrics()
|
||||
|
||||
it("doesn't log the request", function () {
|
||||
sinon.assert.notCalled(this.logger.debug)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with custom log fields', function () {
|
||||
beforeEach('add custom fields', function () {
|
||||
this.req.logger.addFields({ a: 1, b: 2 })
|
||||
})
|
||||
|
||||
endRequest()
|
||||
|
||||
it('logs the request with the custom log fields', function () {
|
||||
sinon.assert.calledWith(
|
||||
this.logger.debug,
|
||||
{ req: this.req, res: this.res, responseTimeMs: 500, a: 1, b: 2 },
|
||||
'%s %s',
|
||||
this.req.method,
|
||||
this.req.url
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('when setting the log level', function () {
|
||||
beforeEach('set custom level', function () {
|
||||
this.req.logger.setLevel('warn')
|
||||
})
|
||||
|
||||
endRequest()
|
||||
|
||||
it('logs the request at the custom level', function () {
|
||||
sinon.assert.calledWith(
|
||||
this.logger.warn,
|
||||
{ req: this.req, res: this.res, responseTimeMs: 500 },
|
||||
'%s %s',
|
||||
this.req.method,
|
||||
this.req.url
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a different default log level', function () {
|
||||
beforeEach('set up the monitor', function (done) {
|
||||
this.http.monitor(this.logger, 'info')(this.req, this.res, done)
|
||||
})
|
||||
|
||||
endRequest()
|
||||
|
||||
it('logs the request at that level', function () {
|
||||
sinon.assert.calledWith(
|
||||
this.logger.info,
|
||||
{ req: this.req, res: this.res, responseTimeMs: 500 },
|
||||
'%s %s',
|
||||
this.req.method,
|
||||
this.req.url
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
function endRequest() {
|
||||
beforeEach('end the request', function () {
|
||||
this.clock.tick(500)
|
||||
this.res.end(this.data)
|
||||
})
|
||||
}
|
||||
|
||||
function expectOriginalEndCalled() {
|
||||
it('calls the original res.end()', function () {
|
||||
sinon.assert.calledWith(this.originalResponseEnd, this.data)
|
||||
})
|
||||
}
|
||||
|
||||
function expectMetrics() {
|
||||
it('records the response time', function () {
|
||||
sinon.assert.calledWith(this.Metrics.timing, 'http_request', 500, null, {
|
||||
method: this.req.method,
|
||||
status_code: this.res.status_code,
|
||||
path: 'project_id_cleanup',
|
||||
})
|
||||
})
|
||||
|
||||
it('records the request size', function () {
|
||||
sinon.assert.calledWith(
|
||||
this.Metrics.summary,
|
||||
'http_request_size_bytes',
|
||||
123,
|
||||
{
|
||||
method: this.req.method,
|
||||
status_code: this.res.status_code,
|
||||
path: 'project_id_cleanup',
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
89
libraries/metrics/test/unit/js/mongodb.js
Normal file
89
libraries/metrics/test/unit/js/mongodb.js
Normal file
@@ -0,0 +1,89 @@
|
||||
const Metrics = require('../../..')
|
||||
|
||||
const { expect } = require('chai')
|
||||
const prom = require('prom-client')
|
||||
|
||||
describe('mongodb', function () {
|
||||
beforeEach(function () {
|
||||
prom.register.clear()
|
||||
this.pool = {
|
||||
totalConnectionCount: 8,
|
||||
availableConnectionCount: 2,
|
||||
waitQueueSize: 4,
|
||||
options: { maxPoolSize: 10 },
|
||||
}
|
||||
this.servers = new Map([['server1', { s: { pool: this.pool } }]])
|
||||
|
||||
this.mongoClient = { topology: { s: { servers: this.servers } } }
|
||||
})
|
||||
|
||||
it('handles an unconnected client', async function () {
|
||||
const mongoClient = {}
|
||||
Metrics.mongodb.monitor(mongoClient)
|
||||
const metrics = await getMetrics()
|
||||
expect(metrics).to.deep.equal({})
|
||||
})
|
||||
|
||||
it('collects Mongo metrics', async function () {
|
||||
Metrics.mongodb.monitor(this.mongoClient)
|
||||
const metrics = await getMetrics()
|
||||
expect(metrics).to.deep.equal({
|
||||
'mongo_connection_pool_max:server1': 10,
|
||||
'mongo_connection_pool_size:server1': 8,
|
||||
'mongo_connection_pool_available:server1': 2,
|
||||
'mongo_connection_pool_waiting:server1': 4,
|
||||
})
|
||||
})
|
||||
|
||||
it('handles topology changes', async function () {
|
||||
Metrics.mongodb.monitor(this.mongoClient)
|
||||
let metrics = await getMetrics()
|
||||
expect(metrics).to.deep.equal({
|
||||
'mongo_connection_pool_max:server1': 10,
|
||||
'mongo_connection_pool_size:server1': 8,
|
||||
'mongo_connection_pool_available:server1': 2,
|
||||
'mongo_connection_pool_waiting:server1': 4,
|
||||
})
|
||||
|
||||
// Add a server
|
||||
this.servers.set('server2', this.servers.get('server1'))
|
||||
metrics = await getMetrics()
|
||||
expect(metrics).to.deep.equal({
|
||||
'mongo_connection_pool_max:server1': 10,
|
||||
'mongo_connection_pool_size:server1': 8,
|
||||
'mongo_connection_pool_available:server1': 2,
|
||||
'mongo_connection_pool_waiting:server1': 4,
|
||||
'mongo_connection_pool_max:server2': 10,
|
||||
'mongo_connection_pool_size:server2': 8,
|
||||
'mongo_connection_pool_available:server2': 2,
|
||||
'mongo_connection_pool_waiting:server2': 4,
|
||||
})
|
||||
|
||||
// Delete a server
|
||||
this.servers.delete('server1')
|
||||
metrics = await getMetrics()
|
||||
expect(metrics).to.deep.equal({
|
||||
'mongo_connection_pool_max:server2': 10,
|
||||
'mongo_connection_pool_size:server2': 8,
|
||||
'mongo_connection_pool_available:server2': 2,
|
||||
'mongo_connection_pool_waiting:server2': 4,
|
||||
})
|
||||
|
||||
// Delete another server
|
||||
this.servers.delete('server2')
|
||||
metrics = await getMetrics()
|
||||
expect(metrics).to.deep.equal({})
|
||||
})
|
||||
})
|
||||
|
||||
async function getMetrics() {
|
||||
const metrics = await prom.register.getMetricsAsJSON()
|
||||
const result = {}
|
||||
for (const metric of metrics) {
|
||||
for (const value of metric.values) {
|
||||
const key = `${metric.name}:${value.labels.mongo_server}`
|
||||
result[key] = value.value
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
Reference in New Issue
Block a user