metrics_tests.js 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343
  1. const { promisify } = require('node:util')
  2. const os = require('node:os')
  3. const http = require('node:http')
  4. const { expect } = require('chai')
  5. const Metrics = require('../..')
  6. const HOSTNAME = os.hostname()
  7. const APP_NAME = 'test-app'
  8. const sleep = promisify(setTimeout)
  9. describe('Metrics module', function () {
  10. before(function () {
  11. process.env.METRICS_APP_NAME = 'test-app'
  12. require('../../initialize')
  13. })
  14. describe('at startup', function () {
  15. it('increments the process_startup counter', async function () {
  16. await expectMetricValue('process_startup', 1)
  17. })
  18. it('collects default metrics', async function () {
  19. const metric = await getMetric('process_cpu_user_seconds_total')
  20. expect(metric).to.exist
  21. })
  22. })
  23. describe('inc()', function () {
  24. it('increments counts by 1', async function () {
  25. Metrics.inc('duck_count')
  26. await expectMetricValue('duck_count', 1)
  27. Metrics.inc('duck_count')
  28. Metrics.inc('duck_count')
  29. await expectMetricValue('duck_count', 3)
  30. })
  31. it('escapes special characters in the key', async function () {
  32. Metrics.inc('show.me the $!!')
  33. await expectMetricValue('show_me_the____', 1)
  34. })
  35. })
  36. describe('count()', function () {
  37. it('increments counts by the given count', async function () {
  38. Metrics.count('rabbit_count', 5)
  39. await expectMetricValue('rabbit_count', 5)
  40. Metrics.count('rabbit_count', 6)
  41. Metrics.count('rabbit_count', 7)
  42. await expectMetricValue('rabbit_count', 18)
  43. })
  44. })
  45. describe('summary()', function () {
  46. it('collects observations', async function () {
  47. Metrics.summary('oven_temp', 200)
  48. Metrics.summary('oven_temp', 300)
  49. Metrics.summary('oven_temp', 450)
  50. const sum = await getSummarySum('oven_temp')
  51. expect(sum).to.equal(950)
  52. })
  53. })
  54. describe('timing()', function () {
  55. it('collects timings', async function () {
  56. Metrics.timing('sprint_100m', 10)
  57. Metrics.timing('sprint_100m', 20)
  58. Metrics.timing('sprint_100m', 30)
  59. const sum = await getSummarySum('timer_sprint_100m')
  60. expect(sum).to.equal(60)
  61. })
  62. })
  63. describe('histogram()', function () {
  64. it('collects in buckets', async function () {
  65. const buckets = [10, 100, 1000]
  66. Metrics.histogram('distance', 10, buckets)
  67. Metrics.histogram('distance', 20, buckets)
  68. Metrics.histogram('distance', 100, buckets)
  69. Metrics.histogram('distance', 200, buckets)
  70. Metrics.histogram('distance', 1000, buckets)
  71. Metrics.histogram('distance', 2000, buckets)
  72. const sum = await getSummarySum('histogram_distance')
  73. expect(sum).to.equal(3330)
  74. await checkHistogramValues('histogram_distance', {
  75. 10: 1,
  76. 100: 3,
  77. 1000: 5,
  78. '+Inf': 6,
  79. })
  80. })
  81. })
  82. describe('Timer', function () {
  83. beforeEach('collect timings', async function () {
  84. const buckets = [10, 100, 1000]
  85. for (const duration of [1, 1, 1, 15, 15, 15, 105, 105, 105]) {
  86. const withBuckets = new Metrics.Timer(
  87. 'height',
  88. 1,
  89. { label_1: 'a' },
  90. buckets
  91. )
  92. const withOutBuckets = new Metrics.Timer('depth', 1, { label_2: 'b' })
  93. await sleep(duration)
  94. withBuckets.done()
  95. withOutBuckets.done({ label_3: 'c' })
  96. }
  97. })
  98. it('with buckets', async function () {
  99. await checkHistogramValues('histogram_height', {
  100. 10: 3,
  101. 100: 6,
  102. 1000: 9,
  103. '+Inf': 9,
  104. })
  105. const labelNames = await getMetric('histogram_height').labelNames
  106. expect(labelNames).to.deep.equal(['label_1'])
  107. })
  108. it('without buckets', async function () {
  109. await checkSummaryValues('timer_depth', {
  110. 0.01: 1,
  111. 0.05: 1,
  112. 0.5: 15,
  113. 0.9: 105,
  114. 0.95: 105,
  115. 0.99: 105,
  116. 0.999: 105,
  117. })
  118. const labelNames = await getMetric('timer_depth').labelNames
  119. expect(labelNames).to.deep.equal(['label_2', 'label_3'])
  120. })
  121. })
  122. describe('gauge()', function () {
  123. it('records values', async function () {
  124. Metrics.gauge('water_level', 1.5)
  125. await expectMetricValue('water_level', 1.5)
  126. Metrics.gauge('water_level', 4.2)
  127. await expectMetricValue('water_level', 4.2)
  128. })
  129. })
  130. describe('globalGauge()', function () {
  131. it('records values without a host label', async function () {
  132. Metrics.globalGauge('tire_pressure', 99.99)
  133. const { value, labels } = await getMetricValue('tire_pressure')
  134. expect(value).to.equal(99.99)
  135. expect(labels.host).to.equal('global')
  136. expect(labels.app).to.equal(APP_NAME)
  137. })
  138. })
  139. describe('open_sockets', function () {
  140. const keyServer1 = 'open_connections_http_127_42_42_1'
  141. const keyServer2 = 'open_connections_http_127_42_42_2'
  142. let finish1, finish2, emitResponse1, emitResponse2
  143. function resetEmitResponse1() {
  144. emitResponse1 = new Promise(resolve => (finish1 = resolve))
  145. }
  146. resetEmitResponse1()
  147. function resetEmitResponse2() {
  148. emitResponse2 = new Promise(resolve => (finish2 = resolve))
  149. }
  150. resetEmitResponse2()
  151. let server1, server2
  152. before(function setupServer1(done) {
  153. server1 = http.createServer((req, res) => {
  154. res.write('...')
  155. emitResponse1.then(() => res.end())
  156. })
  157. server1.listen(0, '127.42.42.1', done)
  158. })
  159. before(function setupServer2(done) {
  160. server2 = http.createServer((req, res) => {
  161. res.write('...')
  162. emitResponse2.then(() => res.end())
  163. })
  164. server2.listen(0, '127.42.42.2', done)
  165. })
  166. after(function cleanupPendingRequests() {
  167. finish1()
  168. finish2()
  169. })
  170. after(function shutdownServer1(done) {
  171. if (server1) server1.close(done)
  172. })
  173. after(function shutdownServer2(done) {
  174. if (server2) server2.close(done)
  175. })
  176. let urlServer1, urlServer2
  177. before(function setUrls() {
  178. urlServer1 = `http://127.42.42.1:${server1.address().port}/`
  179. urlServer2 = `http://127.42.42.2:${server2.address().port}/`
  180. })
  181. describe('gaugeOpenSockets()', function () {
  182. beforeEach(function runGaugeOpenSockets() {
  183. Metrics.open_sockets.gaugeOpenSockets(true)
  184. })
  185. describe('without pending connections', function () {
  186. it('emits no open_connections', async function () {
  187. await expectNoMetricValue(keyServer1)
  188. await expectNoMetricValue(keyServer2)
  189. })
  190. })
  191. describe('with pending connections for server1', function () {
  192. before(function (done) {
  193. http.get(urlServer1)
  194. http.get(urlServer1)
  195. setTimeout(done, 10)
  196. })
  197. it('emits 2 open_connections for server1', async function () {
  198. await expectMetricValue(keyServer1, 2)
  199. })
  200. it('emits no open_connections for server2', async function () {
  201. await expectNoMetricValue(keyServer2)
  202. })
  203. })
  204. describe('with pending connections for server1 and server2', function () {
  205. before(function (done) {
  206. http.get(urlServer2)
  207. http.get(urlServer2)
  208. setTimeout(done, 10)
  209. })
  210. it('emits 2 open_connections for server1', async function () {
  211. await expectMetricValue(keyServer1, 2)
  212. })
  213. it('emits 2 open_connections for server2', async function () {
  214. await expectMetricValue(keyServer2, 2)
  215. })
  216. })
  217. describe('when requests finish for server1', function () {
  218. before(function (done) {
  219. finish1()
  220. resetEmitResponse1()
  221. http.get(urlServer1)
  222. setTimeout(done, 10)
  223. })
  224. it('emits 1 open_connections for server1', async function () {
  225. await expectMetricValue(keyServer1, 1)
  226. })
  227. it('emits 2 open_connections for server2', async function () {
  228. await expectMetricValue(keyServer2, 2)
  229. })
  230. })
  231. describe('when all requests complete', function () {
  232. before(function (done) {
  233. finish1()
  234. finish2()
  235. setTimeout(done, 10)
  236. })
  237. it('emits no open_connections', async function () {
  238. await expectNoMetricValue(keyServer1)
  239. await expectNoMetricValue(keyServer2)
  240. })
  241. })
  242. })
  243. })
  244. })
  245. function getMetric(key) {
  246. return Metrics.register.getSingleMetric(key)
  247. }
  248. async function getSummarySum(key) {
  249. const metric = getMetric(key)
  250. const item = await metric.get()
  251. for (const value of item.values) {
  252. if (value.metricName === `${key}_sum`) {
  253. return value.value
  254. }
  255. }
  256. return null
  257. }
  258. async function checkHistogramValues(key, values) {
  259. const metric = getMetric(key)
  260. const item = await metric.get()
  261. const found = {}
  262. for (const value of item.values) {
  263. const bucket = value.labels.le
  264. if (!bucket) continue
  265. found[bucket] = value.value
  266. }
  267. expect(found).to.deep.equal(values)
  268. return null
  269. }
  270. async function checkSummaryValues(key, values) {
  271. const metric = getMetric(key)
  272. const item = await metric.get()
  273. const found = {}
  274. for (const value of item.values) {
  275. const quantile = value.labels.quantile
  276. if (!quantile) continue
  277. found[quantile] = value.value
  278. }
  279. for (const quantile of Object.keys(values)) {
  280. expect(found[quantile]).to.be.within(
  281. values[quantile] - 5,
  282. values[quantile] + 5,
  283. `quantile: ${quantile}`
  284. )
  285. }
  286. return null
  287. }
  288. async function getMetricValue(key) {
  289. const metrics = await Metrics.register.getMetricsAsJSON()
  290. const metric = metrics.find(m => m.name === key)
  291. return metric.values[0]
  292. }
  293. async function expectMetricValue(key, expectedValue) {
  294. const value = await getMetricValue(key)
  295. expect(value.value).to.equal(expectedValue)
  296. expect(value.labels.host).to.equal(HOSTNAME)
  297. expect(value.labels.app).to.equal(APP_NAME)
  298. }
  299. async function expectNoMetricValue(key) {
  300. const metric = getMetric(key)
  301. if (!metric) return
  302. await expectMetricValue(key, 0)
  303. }