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