# SPDX-License-Identifier: AGPL-3.0-or-later # pylint: disable=missing-module-docstring import decimal import threading from searx import logger __all__ = ["Histogram", "HistogramStorage", "CounterStorage"] logger = logger.getChild('searx.metrics') class Histogram: # pylint: disable=missing-class-docstring _slots__ = '_lock', '_size', '_sum', '_quartiles', '_count', '_width' def __init__(self, width=10, size=200): self._lock = threading.Lock() self._width = width self._size = size self._quartiles = [0] * size self._count = 0 self._sum = 0 def observe(self, value): q = int(value / self._width) if q < 0: # pylint: disable=consider-using-max-builtin # Value below zero is ignored q = 0 if q >= self._size: # Value above the maximum is replaced by the maximum q = self._size - 1 with self._lock: self._quartiles[q] += 1 self._count += 1 self._sum += value @property def quartiles(self): return list(self._quartiles) @property def count(self): return self._count @property def sum(self): return self._sum @property def average(self): with self._lock: if self._count != 0: return self._sum / self._count return 0 @property def quartile_percentage(self): '''Quartile in percentage''' with self._lock: if self._count > 0: return [int(q * 100 / self._count) for q in self._quartiles] return self._quartiles @property def quartile_percentage_map(self): result = {} # use Decimal to avoid rounding errors x = decimal.Decimal(0) width = decimal.Decimal(self._width) width_exponent = -width.as_tuple().exponent with self._lock: if self._count > 0: for y in self._quartiles: yp = int(y * 100 / self._count) # pylint: disable=invalid-name if yp != 0: result[round(float(x), width_exponent)] = yp x += width return result def percentage(self, percentage): # use Decimal to avoid rounding errors x = decimal.Decimal(0) width = decimal.Decimal(self._width) stop_at_value = decimal.Decimal(self._count) / 100 * percentage sum_value = 0 with self._lock: if self._count > 0: for y in self._quartiles: sum_value += y if sum_value >= stop_at_value: return x x += width return None def __repr__(self): return "Histogram" class HistogramStorage: # pylint: disable=missing-class-docstring __slots__ = 'measures', 'histogram_class' def __init__(self, histogram_class=Histogram): self.clear() self.histogram_class = histogram_class def clear(self): self.measures = {} def configure(self, width, size, *args): measure = self.histogram_class(width, size) self.measures[args] = measure return measure def get(self, *args): return self.measures.get(args, None) def dump(self): logger.debug("Histograms:") ks = sorted(self.measures.keys(), key='/'.join) # pylint: disable=invalid-name for k in ks: logger.debug("- %-60s %s", '|'.join(k), self.measures[k]) class CounterStorage: # pylint: disable=missing-class-docstring __slots__ = 'counters', 'lock' def __init__(self): self.lock = threading.Lock() self.clear() def clear(self): with self.lock: self.counters = {} def configure(self, *args): with self.lock: self.counters[args] = 0 def get(self, *args): return self.counters[args] def add(self, value, *args): with self.lock: self.counters[args] += value def dump(self): with self.lock: ks = sorted(self.counters.keys(), key='/'.join) # pylint: disable=invalid-name logger.debug("Counters:") for k in ks: logger.debug("- %-60s %s", '|'.join(k), self.counters[k]) class VoidHistogram(Histogram): # pylint: disable=missing-class-docstring def observe(self, value): pass class VoidCounterStorage(CounterStorage): # pylint: disable=missing-class-docstring def add(self, value, *args): pass