models.py 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167
  1. # SPDX-License-Identifier: AGPL-3.0-or-later
  2. import decimal
  3. import threading
  4. from searx import logger
  5. __all__ = ["Histogram", "HistogramStorage", "CounterStorage"]
  6. logger = logger.getChild('searx.metrics')
  7. class Histogram:
  8. _slots__ = '_lock', '_size', '_sum', '_quartiles', '_count', '_width'
  9. def __init__(self, width=10, size=200):
  10. self._lock = threading.Lock()
  11. self._width = width
  12. self._size = size
  13. self._quartiles = [0] * size
  14. self._count = 0
  15. self._sum = 0
  16. def observe(self, value):
  17. q = int(value / self._width)
  18. if q < 0:
  19. """Value below zero is ignored"""
  20. q = 0
  21. if q >= self._size:
  22. """Value above the maximum is replaced by the maximum"""
  23. q = self._size - 1
  24. with self._lock:
  25. self._quartiles[q] += 1
  26. self._count += 1
  27. self._sum += value
  28. @property
  29. def quartiles(self):
  30. return list(self._quartiles)
  31. @property
  32. def count(self):
  33. return self._count
  34. @property
  35. def sum(self):
  36. return self._sum
  37. @property
  38. def average(self):
  39. with self._lock:
  40. if self._count != 0:
  41. return self._sum / self._count
  42. else:
  43. return 0
  44. @property
  45. def quartile_percentage(self):
  46. '''Quartile in percentage'''
  47. with self._lock:
  48. if self._count > 0:
  49. return [int(q * 100 / self._count) for q in self._quartiles]
  50. else:
  51. return self._quartiles
  52. @property
  53. def quartile_percentage_map(self):
  54. result = {}
  55. # use Decimal to avoid rounding errors
  56. x = decimal.Decimal(0)
  57. width = decimal.Decimal(self._width)
  58. width_exponent = -width.as_tuple().exponent
  59. with self._lock:
  60. if self._count > 0:
  61. for y in self._quartiles:
  62. yp = int(y * 100 / self._count)
  63. if yp != 0:
  64. result[round(float(x), width_exponent)] = yp
  65. x += width
  66. return result
  67. def percentage(self, percentage):
  68. # use Decimal to avoid rounding errors
  69. x = decimal.Decimal(0)
  70. width = decimal.Decimal(self._width)
  71. stop_at_value = decimal.Decimal(self._count) / 100 * percentage
  72. sum_value = 0
  73. with self._lock:
  74. if self._count > 0:
  75. for y in self._quartiles:
  76. sum_value += y
  77. if sum_value >= stop_at_value:
  78. return x
  79. x += width
  80. return None
  81. def __repr__(self):
  82. return "Histogram<avg: " + str(self.average) + ", count: " + str(self._count) + ">"
  83. class HistogramStorage:
  84. __slots__ = 'measures', 'histogram_class'
  85. def __init__(self, histogram_class=Histogram):
  86. self.clear()
  87. self.histogram_class = histogram_class
  88. def clear(self):
  89. self.measures = {}
  90. def configure(self, width, size, *args):
  91. measure = self.histogram_class(width, size)
  92. self.measures[args] = measure
  93. return measure
  94. def get(self, *args):
  95. return self.measures.get(args, None)
  96. def dump(self):
  97. logger.debug("Histograms:")
  98. ks = sorted(self.measures.keys(), key='/'.join)
  99. for k in ks:
  100. logger.debug("- %-60s %s", '|'.join(k), self.measures[k])
  101. class CounterStorage:
  102. __slots__ = 'counters', 'lock'
  103. def __init__(self):
  104. self.lock = threading.Lock()
  105. self.clear()
  106. def clear(self):
  107. with self.lock:
  108. self.counters = {}
  109. def configure(self, *args):
  110. with self.lock:
  111. self.counters[args] = 0
  112. def get(self, *args):
  113. return self.counters[args]
  114. def add(self, value, *args):
  115. with self.lock:
  116. self.counters[args] += value
  117. def dump(self):
  118. with self.lock:
  119. ks = sorted(self.counters.keys(), key='/'.join)
  120. logger.debug("Counters:")
  121. for k in ks:
  122. logger.debug("- %-60s %s", '|'.join(k), self.counters[k])
  123. class VoidHistogram(Histogram):
  124. def observe(self, value):
  125. pass
  126. class VoidCounterStorage(CounterStorage):
  127. def add(self, value, *args):
  128. pass