prom_wrapper.js 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177
  1. const logger = require('@overleaf/logger')
  2. const prom = require('prom-client')
  3. const registry = require('prom-client').register
  4. const metrics = new Map()
  5. const labelsKey = function (labels) {
  6. let keys = Object.keys(labels)
  7. if (keys.length === 0) {
  8. return ''
  9. }
  10. keys = keys.sort()
  11. let hash = ''
  12. for (const key of keys) {
  13. if (hash.length) {
  14. hash += ','
  15. }
  16. hash += `${key}:${labels[key]}`
  17. }
  18. return hash
  19. }
  20. const labelsAsArgs = function (labels, labelNames) {
  21. const args = []
  22. for (const label of labelNames) {
  23. args.push(labels[label] || '')
  24. }
  25. return args
  26. }
  27. const PromWrapper = {
  28. ttlInMinutes: 0,
  29. registry,
  30. metric(type, name, labels, buckets) {
  31. return metrics.get(name) || new MetricWrapper(type, name, labels, buckets)
  32. },
  33. collectDefaultMetrics: prom.collectDefaultMetrics,
  34. }
  35. class MetricWrapper {
  36. constructor(type, name, labels, buckets) {
  37. metrics.set(name, this)
  38. this.name = name
  39. this.instances = new Map()
  40. this.lastAccess = new Date()
  41. const labelNames = labels ? Object.keys(labels) : []
  42. switch (type) {
  43. case 'counter':
  44. this.metric = new prom.Counter({
  45. name,
  46. help: name,
  47. labelNames,
  48. })
  49. break
  50. case 'histogram':
  51. this.metric = new prom.Histogram({
  52. name,
  53. help: name,
  54. labelNames,
  55. buckets,
  56. })
  57. break
  58. case 'summary':
  59. this.metric = new prom.Summary({
  60. name,
  61. help: name,
  62. maxAgeSeconds: 60,
  63. ageBuckets: 10,
  64. labelNames,
  65. })
  66. break
  67. case 'gauge':
  68. this.metric = new prom.Gauge({
  69. name,
  70. help: name,
  71. labelNames,
  72. })
  73. break
  74. default:
  75. throw new Error(`Unknown metric type: ${type}`)
  76. }
  77. }
  78. inc(labels, value) {
  79. this._execMethod('inc', labels, value)
  80. }
  81. observe(labels, value) {
  82. this._execMethod('observe', labels, value)
  83. }
  84. set(labels, value) {
  85. this._execMethod('set', labels, value)
  86. }
  87. sweep() {
  88. const thresh = new Date(Date.now() - 1000 * 60 * PromWrapper.ttlInMinutes)
  89. this.instances.forEach((instance, key) => {
  90. if (thresh > instance.time) {
  91. if (process.env.DEBUG_METRICS) {
  92. // eslint-disable-next-line no-console
  93. console.log(
  94. 'Sweeping stale metric instance',
  95. this.name,
  96. { labels: instance.labels },
  97. key
  98. )
  99. }
  100. this.metric.remove(
  101. ...labelsAsArgs(instance.labels, this.metric.labelNames)
  102. )
  103. }
  104. })
  105. if (thresh > this.lastAccess) {
  106. if (process.env.DEBUG_METRICS) {
  107. // eslint-disable-next-line no-console
  108. console.log('Sweeping stale metric', this.name, thresh, this.lastAccess)
  109. }
  110. metrics.delete(this.name)
  111. registry.removeSingleMetric(this.name)
  112. }
  113. }
  114. _execMethod(method, labels, value) {
  115. const key = labelsKey(labels)
  116. if (key !== '') {
  117. this.instances.set(key, { time: new Date(), labels })
  118. }
  119. this.lastAccess = new Date()
  120. try {
  121. this.metric[method](labels, value)
  122. } catch (err) {
  123. logger.warn(
  124. { err, metric: this.metric.name, labels },
  125. 'failed to record metric'
  126. )
  127. }
  128. }
  129. }
  130. let sweepingInterval
  131. PromWrapper.setupSweeping = function () {
  132. if (sweepingInterval) {
  133. clearInterval(sweepingInterval)
  134. }
  135. if (!PromWrapper.ttlInMinutes) {
  136. if (process.env.DEBUG_METRICS) {
  137. // eslint-disable-next-line no-console
  138. console.log('Not registering sweep method -- empty ttl')
  139. }
  140. return
  141. }
  142. if (process.env.DEBUG_METRICS) {
  143. // eslint-disable-next-line no-console
  144. console.log('Registering sweep method')
  145. }
  146. sweepingInterval = setInterval(function () {
  147. if (process.env.DEBUG_METRICS) {
  148. // eslint-disable-next-line no-console
  149. console.log('Sweeping metrics')
  150. }
  151. metrics.forEach((metric, key) => {
  152. metric.sweep()
  153. })
  154. }, 60000)
  155. const Metrics = require('./index')
  156. Metrics.registerDestructor(() => clearInterval(sweepingInterval))
  157. }
  158. module.exports = PromWrapper