prom_wrapper.js 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194
  1. /*
  2. * decaffeinate suggestions:
  3. * DS101: Remove unnecessary use of Array.from
  4. * DS102: Remove unnecessary code created because of implicit returns
  5. * DS205: Consider reworking code to avoid use of IIFEs
  6. * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  7. */
  8. const prom = require('prom-client')
  9. const registry = require('prom-client').register
  10. const metrics = new Map()
  11. const optsKey = function (opts) {
  12. let keys = Object.keys(opts)
  13. if (keys.length === 0) {
  14. return ''
  15. }
  16. keys = keys.sort()
  17. let hash = ''
  18. for (const key of Array.from(keys)) {
  19. if (hash.length) {
  20. hash += ','
  21. }
  22. hash += `${key}:${opts[key]}`
  23. }
  24. return hash
  25. }
  26. const extendOpts = function (opts, labelNames) {
  27. // Make a clone in order to be able to re-use opts for other kinds of metrics.
  28. opts = Object.assign({}, opts)
  29. for (const label of Array.from(labelNames)) {
  30. if (!opts[label]) {
  31. opts[label] = ''
  32. }
  33. }
  34. return opts
  35. }
  36. const optsAsArgs = function (opts, labelNames) {
  37. const args = []
  38. for (const label of Array.from(labelNames)) {
  39. args.push(opts[label] || '')
  40. }
  41. return args
  42. }
  43. const PromWrapper = {
  44. ttlInMinutes: 0,
  45. registry,
  46. metric(type, name, buckets) {
  47. return metrics.get(name) || new MetricWrapper(type, name, buckets)
  48. },
  49. collectDefaultMetrics: prom.collectDefaultMetrics,
  50. }
  51. class MetricWrapper {
  52. constructor(type, name, buckets) {
  53. metrics.set(name, this)
  54. this.name = name
  55. this.instances = new Map()
  56. this.lastAccess = new Date()
  57. this.metric = (() => {
  58. switch (type) {
  59. case 'counter':
  60. return new prom.Counter({
  61. name,
  62. help: name,
  63. labelNames: ['status', 'method', 'path'],
  64. })
  65. case 'histogram':
  66. return new prom.Histogram({
  67. name,
  68. help: name,
  69. labelNames: [
  70. 'path',
  71. 'status_code',
  72. 'method',
  73. 'collection',
  74. 'query',
  75. ],
  76. buckets,
  77. })
  78. case 'summary':
  79. return new prom.Summary({
  80. name,
  81. help: name,
  82. maxAgeSeconds: 60,
  83. ageBuckets: 10,
  84. labelNames: [
  85. 'path',
  86. 'status_code',
  87. 'method',
  88. 'collection',
  89. 'query',
  90. ],
  91. })
  92. case 'gauge':
  93. return new prom.Gauge({
  94. name,
  95. help: name,
  96. labelNames: ['host', 'status'],
  97. })
  98. }
  99. })()
  100. }
  101. inc(opts, value) {
  102. return this._execMethod('inc', opts, value)
  103. }
  104. observe(opts, value) {
  105. return this._execMethod('observe', opts, value)
  106. }
  107. set(opts, value) {
  108. return this._execMethod('set', opts, value)
  109. }
  110. sweep() {
  111. const thresh = new Date(Date.now() - 1000 * 60 * PromWrapper.ttlInMinutes)
  112. this.instances.forEach((instance, key) => {
  113. if (thresh > instance.time) {
  114. if (process.env.DEBUG_METRICS) {
  115. // eslint-disable-next-line no-console
  116. console.log(
  117. 'Sweeping stale metric instance',
  118. this.name,
  119. { opts: instance.opts },
  120. key
  121. )
  122. }
  123. return this.metric.remove(
  124. ...Array.from(optsAsArgs(instance.opts, this.metric.labelNames) || [])
  125. )
  126. }
  127. })
  128. if (thresh > this.lastAccess) {
  129. if (process.env.DEBUG_METRICS) {
  130. // eslint-disable-next-line no-console
  131. console.log('Sweeping stale metric', this.name, thresh, this.lastAccess)
  132. }
  133. metrics.delete(this.name)
  134. return registry.removeSingleMetric(this.name)
  135. }
  136. }
  137. _execMethod(method, opts, value) {
  138. opts = extendOpts(opts, this.metric.labelNames)
  139. const key = optsKey(opts)
  140. if (key !== '') {
  141. this.instances.set(key, { time: new Date(), opts })
  142. }
  143. this.lastAccess = new Date()
  144. return this.metric[method](opts, value)
  145. }
  146. }
  147. let sweepingInterval
  148. PromWrapper.setupSweeping = function () {
  149. if (sweepingInterval) {
  150. clearInterval(sweepingInterval)
  151. }
  152. if (!PromWrapper.ttlInMinutes) {
  153. if (process.env.DEBUG_METRICS) {
  154. // eslint-disable-next-line no-console
  155. console.log('Not registering sweep method -- empty ttl')
  156. }
  157. return
  158. }
  159. if (process.env.DEBUG_METRICS) {
  160. // eslint-disable-next-line no-console
  161. console.log('Registering sweep method')
  162. }
  163. sweepingInterval = setInterval(function () {
  164. if (process.env.DEBUG_METRICS) {
  165. // eslint-disable-next-line no-console
  166. console.log('Sweeping metrics')
  167. }
  168. return metrics.forEach((metric, key) => {
  169. return metric.sweep()
  170. })
  171. }, 60000)
  172. const Metrics = require('./index')
  173. Metrics.registerDestructor(() => clearInterval(sweepingInterval))
  174. }
  175. module.exports = PromWrapper