models.py 4.6 KB

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