2021-04-14 15:23:15 +00:00
|
|
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
2024-03-11 13:06:26 +00:00
|
|
|
# pylint: disable=missing-module-docstring
|
2021-04-14 15:23:15 +00:00
|
|
|
|
|
|
|
import decimal
|
|
|
|
import threading
|
|
|
|
|
|
|
|
from searx import logger
|
|
|
|
|
|
|
|
|
|
|
|
__all__ = ["Histogram", "HistogramStorage", "CounterStorage"]
|
|
|
|
|
|
|
|
logger = logger.getChild('searx.metrics')
|
|
|
|
|
|
|
|
|
2024-03-11 13:06:26 +00:00
|
|
|
class Histogram: # pylint: disable=missing-class-docstring
|
2021-04-14 15:23:15 +00:00
|
|
|
|
|
|
|
_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)
|
2024-03-11 13:06:26 +00:00
|
|
|
if q < 0: # pylint: disable=consider-using-max-builtin
|
|
|
|
# Value below zero is ignored
|
2021-04-14 15:23:15 +00:00
|
|
|
q = 0
|
|
|
|
if q >= self._size:
|
2024-03-11 13:06:26 +00:00
|
|
|
# Value above the maximum is replaced by the maximum
|
2021-04-14 15:23:15 +00:00
|
|
|
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
|
2024-03-11 13:06:26 +00:00
|
|
|
return 0
|
2021-04-14 15:23:15 +00:00
|
|
|
|
|
|
|
@property
|
|
|
|
def quartile_percentage(self):
|
2021-12-27 08:26:22 +00:00
|
|
|
'''Quartile in percentage'''
|
2021-04-14 15:23:15 +00:00
|
|
|
with self._lock:
|
|
|
|
if self._count > 0:
|
|
|
|
return [int(q * 100 / self._count) for q in self._quartiles]
|
2024-03-11 13:06:26 +00:00
|
|
|
return self._quartiles
|
2021-04-14 15:23:15 +00:00
|
|
|
|
|
|
|
@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:
|
2024-03-11 13:06:26 +00:00
|
|
|
yp = int(y * 100 / self._count) # pylint: disable=invalid-name
|
2021-04-14 15:23:15 +00:00
|
|
|
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<avg: " + str(self.average) + ", count: " + str(self._count) + ">"
|
|
|
|
|
|
|
|
|
2024-03-11 13:06:26 +00:00
|
|
|
class HistogramStorage: # pylint: disable=missing-class-docstring
|
2021-04-14 15:23:15 +00:00
|
|
|
|
2021-12-26 21:44:46 +00:00
|
|
|
__slots__ = 'measures', 'histogram_class'
|
2021-04-14 15:23:15 +00:00
|
|
|
|
2021-12-26 21:44:46 +00:00
|
|
|
def __init__(self, histogram_class=Histogram):
|
2021-04-14 15:23:15 +00:00
|
|
|
self.clear()
|
2021-12-26 21:44:46 +00:00
|
|
|
self.histogram_class = histogram_class
|
2021-04-14 15:23:15 +00:00
|
|
|
|
|
|
|
def clear(self):
|
|
|
|
self.measures = {}
|
|
|
|
|
|
|
|
def configure(self, width, size, *args):
|
2021-12-26 21:44:46 +00:00
|
|
|
measure = self.histogram_class(width, size)
|
2021-04-14 15:23:15 +00:00
|
|
|
self.measures[args] = measure
|
|
|
|
return measure
|
|
|
|
|
|
|
|
def get(self, *args):
|
|
|
|
return self.measures.get(args, None)
|
|
|
|
|
|
|
|
def dump(self):
|
|
|
|
logger.debug("Histograms:")
|
2024-03-11 13:06:26 +00:00
|
|
|
ks = sorted(self.measures.keys(), key='/'.join) # pylint: disable=invalid-name
|
2021-04-14 15:23:15 +00:00
|
|
|
for k in ks:
|
|
|
|
logger.debug("- %-60s %s", '|'.join(k), self.measures[k])
|
|
|
|
|
|
|
|
|
2024-03-11 13:06:26 +00:00
|
|
|
class CounterStorage: # pylint: disable=missing-class-docstring
|
2021-04-14 15:23:15 +00:00
|
|
|
|
|
|
|
__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:
|
2024-03-11 13:06:26 +00:00
|
|
|
ks = sorted(self.counters.keys(), key='/'.join) # pylint: disable=invalid-name
|
2021-04-14 15:23:15 +00:00
|
|
|
logger.debug("Counters:")
|
|
|
|
for k in ks:
|
|
|
|
logger.debug("- %-60s %s", '|'.join(k), self.counters[k])
|
2021-12-26 21:44:46 +00:00
|
|
|
|
|
|
|
|
2024-03-11 13:06:26 +00:00
|
|
|
class VoidHistogram(Histogram): # pylint: disable=missing-class-docstring
|
2021-12-26 21:44:46 +00:00
|
|
|
def observe(self, value):
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
2024-03-11 13:06:26 +00:00
|
|
|
class VoidCounterStorage(CounterStorage): # pylint: disable=missing-class-docstring
|
2021-12-26 21:44:46 +00:00
|
|
|
def add(self, value, *args):
|
|
|
|
pass
|