This commit is contained in:
Alexandre Flament 2024-02-28 21:57:33 +01:00 committed by GitHub
commit 8cb59c6932
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 172 additions and 148 deletions

View file

@ -2,85 +2,57 @@
# lint: pylint # lint: pylint
# pylint: disable=missing-module-docstring # pylint: disable=missing-module-docstring
import typing
import math import math
import contextlib import contextlib
from timeit import default_timer from timeit import default_timer
from operator import itemgetter
from typing import Dict, List, Optional, Any
from typing_extensions import TypedDict
from searx.engines import engines from searx.engines import engines
from .models import HistogramStorage, CounterStorage, VoidHistogram, VoidCounterStorage from .models import Histogram, HistogramStorage, CounterStorage, VoidHistogram, VoidCounterStorage
from .error_recorder import count_error, count_exception, errors_per_engines from .error_recorder import count_error, count_exception, errors_per_engines
__all__ = [ __all__ = [
"initialize", "initialize",
"get_engines_stats", "get_engines_metrics",
"get_engine_errors", "get_engine_errors",
"histogram",
"histogram_observe",
"histogram_observe_time",
"counter",
"counter_inc",
"counter_add",
"count_error", "count_error",
"count_exception", "count_exception",
] ]
ENDPOINTS = {'search'} HISTOGRAM_STORAGE: Optional[HistogramStorage] = None
COUNTER_STORAGE: Optional[CounterStorage] = None
histogram_storage: typing.Optional[HistogramStorage] = None # We do not have a usage of this context manager
counter_storage: typing.Optional[CounterStorage] = None #
# @contextlib.contextmanager
# def histogram_observe_time(*args):
@contextlib.contextmanager # h = histogram_storage.get(*args)
def histogram_observe_time(*args): # before = default_timer()
h = histogram_storage.get(*args) # yield before
before = default_timer() # duration = default_timer() - before
yield before # if h:
duration = default_timer() - before # h.observe(duration)
if h: # else:
h.observe(duration) # raise ValueError("histogram " + repr((*args,)) + " doesn't not exist")
else:
raise ValueError("histogram " + repr((*args,)) + " doesn't not exist")
def histogram_observe(duration, *args):
histogram_storage.get(*args).observe(duration)
def histogram(*args, raise_on_not_found=True):
h = histogram_storage.get(*args)
if raise_on_not_found and h is None:
raise ValueError("histogram " + repr((*args,)) + " doesn't not exist")
return h
def counter_inc(*args):
counter_storage.add(1, *args)
def counter_add(value, *args):
counter_storage.add(value, *args)
def counter(*args):
return counter_storage.get(*args)
def initialize(engine_names=None, enabled=True): def initialize(engine_names=None, enabled=True):
""" """
Initialize metrics Initialize metrics
""" """
global counter_storage, histogram_storage # pylint: disable=global-statement
global COUNTER_STORAGE, HISTOGRAM_STORAGE # pylint: disable=global-statement
if enabled: if enabled:
counter_storage = CounterStorage() COUNTER_STORAGE = CounterStorage()
histogram_storage = HistogramStorage() HISTOGRAM_STORAGE = HistogramStorage()
else: else:
counter_storage = VoidCounterStorage() COUNTER_STORAGE = VoidCounterStorage()
histogram_storage = HistogramStorage(histogram_class=VoidHistogram) HISTOGRAM_STORAGE = HistogramStorage(histogram_class=VoidHistogram)
# max_timeout = max of all the engine.timeout # max_timeout = max of all the engine.timeout
max_timeout = 2 max_timeout = 2
@ -95,22 +67,36 @@ def initialize(engine_names=None, enabled=True):
# engines # engines
for engine_name in engine_names or engines: for engine_name in engine_names or engines:
# search count # search count
counter_storage.configure('engine', engine_name, 'search', 'count', 'sent') COUNTER_STORAGE.configure('engine', engine_name, 'search', 'count', 'sent')
counter_storage.configure('engine', engine_name, 'search', 'count', 'successful') COUNTER_STORAGE.configure('engine', engine_name, 'search', 'count', 'successful')
# global counter of errors # global counter of errors
counter_storage.configure('engine', engine_name, 'search', 'count', 'error') COUNTER_STORAGE.configure('engine', engine_name, 'search', 'count', 'error')
# score of the engine # score of the engine
counter_storage.configure('engine', engine_name, 'score') COUNTER_STORAGE.configure('engine', engine_name, 'score')
# result count per requests # result count per requests
histogram_storage.configure(1, 100, 'engine', engine_name, 'result', 'count') HISTOGRAM_STORAGE.configure(1, 100, 'engine', engine_name, 'result', 'count')
# time doing HTTP requests # time doing HTTP requests
histogram_storage.configure(histogram_width, histogram_size, 'engine', engine_name, 'time', 'http') HISTOGRAM_STORAGE.configure(histogram_width, histogram_size, 'engine', engine_name, 'time', 'http')
# total time # total time
# .time.request and ...response times may overlap .time.http time. # .time.request and ...response times may overlap .time.http time.
histogram_storage.configure(histogram_width, histogram_size, 'engine', engine_name, 'time', 'total') HISTOGRAM_STORAGE.configure(histogram_width, histogram_size, 'engine', engine_name, 'time', 'total')
def get_engine_errors(engline_name_list): class EngineError(TypedDict):
"""Describe an engine error. To do : check the types"""
filename: str
function: str
line_no: int
code: str
exception_classname: str
log_message: str
log_parameters: List[str]
secondary: bool
percentage: int
def get_engine_errors(engline_name_list) -> Dict[str, List[EngineError]]:
result = {} result = {}
engine_names = list(errors_per_engines.keys()) engine_names = list(errors_per_engines.keys())
engine_names.sort() engine_names.sort()
@ -119,7 +105,7 @@ def get_engine_errors(engline_name_list):
continue continue
error_stats = errors_per_engines[engine_name] error_stats = errors_per_engines[engine_name]
sent_search_count = max(counter('engine', engine_name, 'search', 'count', 'sent'), 1) sent_search_count = max(COUNTER_STORAGE.get('engine', engine_name, 'search', 'count', 'sent'), 1)
sorted_context_count_list = sorted(error_stats.items(), key=lambda context_count: context_count[1]) sorted_context_count_list = sorted(error_stats.items(), key=lambda context_count: context_count[1])
r = [] r = []
for context, count in sorted_context_count_list: for context, count in sorted_context_count_list:
@ -141,7 +127,15 @@ def get_engine_errors(engline_name_list):
return result return result
def get_reliabilities(engline_name_list, checker_results): class EngineReliability(TypedDict):
"""Describe the engine reliability. To do: update the checker field type"""
reliability: int
errors: List[EngineError]
checker: Optional[Any]
def get_reliabilities(engline_name_list, checker_results) -> Dict[str, EngineReliability]:
reliabilities = {} reliabilities = {}
engine_errors = get_engine_errors(engline_name_list) engine_errors = get_engine_errors(engline_name_list)
@ -150,7 +144,7 @@ def get_reliabilities(engline_name_list, checker_results):
checker_result = checker_results.get(engine_name, {}) checker_result = checker_results.get(engine_name, {})
checker_success = checker_result.get('success', True) checker_success = checker_result.get('success', True)
errors = engine_errors.get(engine_name) or [] errors = engine_errors.get(engine_name) or []
if counter('engine', engine_name, 'search', 'count', 'sent') == 0: if COUNTER_STORAGE.get('engine', engine_name, 'search', 'count', 'sent') == 0:
# no request # no request
reliability = None reliability = None
elif checker_success and not errors: elif checker_success and not errors:
@ -171,24 +165,55 @@ def get_reliabilities(engline_name_list, checker_results):
return reliabilities return reliabilities
def get_engines_stats(engine_name_list): class EngineStat(TypedDict):
assert counter_storage is not None """Metrics for one engine. To do: check the types"""
assert histogram_storage is not None
name: str
total: Optional[float]
total_p80: Optional[float]
totla_p95: Optional[float]
http: Optional[float]
http_p80: Optional[float]
http_p95: Optional[float]
processing: Optional[float]
processing_p80: Optional[float]
processing_p95: Optional[float]
score: float
score_per_result: float
result_count: int
class EngineStatResult(TypedDict):
"""result of the get_engines_metrics function"""
time: List[EngineStat]
"""List of engine stat"""
max_time: float
"""Maximum response time for all the engines"""
max_result_count: int
"""Maximum number of result for all the engines"""
def get_engines_metrics(engine_name_list) -> EngineStatResult:
assert COUNTER_STORAGE is not None
assert HISTOGRAM_STORAGE is not None
list_time = [] list_time = []
max_time_total = max_result_count = None max_time_total = max_result_count = None
for engine_name in engine_name_list: for engine_name in engine_name_list:
sent_count = counter('engine', engine_name, 'search', 'count', 'sent') sent_count = COUNTER_STORAGE.get('engine', engine_name, 'search', 'count', 'sent')
if sent_count == 0: if sent_count == 0:
continue continue
result_count = histogram('engine', engine_name, 'result', 'count').percentage(50) result_count = HISTOGRAM_STORAGE.get('engine', engine_name, 'result', 'count').percentile(50)
result_count_sum = histogram('engine', engine_name, 'result', 'count').sum result_count_sum = HISTOGRAM_STORAGE.get('engine', engine_name, 'result', 'count').sum
successful_count = counter('engine', engine_name, 'search', 'count', 'successful') successful_count = COUNTER_STORAGE.get('engine', engine_name, 'search', 'count', 'successful')
time_total = histogram('engine', engine_name, 'time', 'total').percentage(50) time_total = HISTOGRAM_STORAGE.get('engine', engine_name, 'time', 'total').percentile(50)
max_time_total = max(time_total or 0, max_time_total or 0) max_time_total = max(time_total or 0, max_time_total or 0)
max_result_count = max(result_count or 0, max_result_count or 0) max_result_count = max(result_count or 0, max_result_count or 0)
@ -209,18 +234,18 @@ def get_engines_stats(engine_name_list):
} }
if successful_count and result_count_sum: if successful_count and result_count_sum:
score = counter('engine', engine_name, 'score') score = COUNTER_STORAGE.get('engine', engine_name, 'score')
stats['score'] = score stats['score'] = score
stats['score_per_result'] = score / float(result_count_sum) stats['score_per_result'] = score / float(result_count_sum)
time_http = histogram('engine', engine_name, 'time', 'http').percentage(50) time_http = HISTOGRAM_STORAGE.get('engine', engine_name, 'time', 'http').percentile(50)
time_http_p80 = time_http_p95 = 0 time_http_p80 = time_http_p95 = 0
if time_http is not None: if time_http is not None:
time_http_p80 = histogram('engine', engine_name, 'time', 'http').percentage(80) time_http_p80 = HISTOGRAM_STORAGE.get('engine', engine_name, 'time', 'http').percentile(80)
time_http_p95 = histogram('engine', engine_name, 'time', 'http').percentage(95) time_http_p95 = HISTOGRAM_STORAGE.get('engine', engine_name, 'time', 'http').percentile(95)
stats['http'] = round(time_http, 1) stats['http'] = round(time_http, 1)
stats['http_p80'] = round(time_http_p80, 1) stats['http_p80'] = round(time_http_p80, 1)
@ -228,8 +253,8 @@ def get_engines_stats(engine_name_list):
if time_total is not None: if time_total is not None:
time_total_p80 = histogram('engine', engine_name, 'time', 'total').percentage(80) time_total_p80 = HISTOGRAM_STORAGE.get('engine', engine_name, 'time', 'total').percentile(80)
time_total_p95 = histogram('engine', engine_name, 'time', 'total').percentage(95) time_total_p95 = HISTOGRAM_STORAGE.get('engine', engine_name, 'time', 'total').percentile(95)
stats['total'] = round(time_total, 1) stats['total'] = round(time_total, 1)
stats['total_p80'] = round(time_total_p80, 1) stats['total_p80'] = round(time_total_p80, 1)

View file

@ -1,7 +1,9 @@
# SPDX-License-Identifier: AGPL-3.0-or-later # SPDX-License-Identifier: AGPL-3.0-or-later
import decimal import decimal
from numbers import Number
import threading import threading
from typing import Dict, List, Optional, Tuple
from searx import logger from searx import logger
@ -15,15 +17,19 @@ class Histogram:
_slots__ = '_lock', '_size', '_sum', '_quartiles', '_count', '_width' _slots__ = '_lock', '_size', '_sum', '_quartiles', '_count', '_width'
def __init__(self, width=10, size=200): def __init__(self, width: int = 10, size: int = 200):
"""
* width: quantile width
* size: number of quantiles
"""
self._lock = threading.Lock() self._lock = threading.Lock()
self._width = width self._width = width
self._size = size self._size = size
self._quartiles = [0] * size self._quartiles = [0] * size
self._count = 0 self._count: int = 0
self._sum = 0 self._sum: int = 0
def observe(self, value): def observe(self, value: Number):
q = int(value / self._width) q = int(value / self._width)
if q < 0: if q < 0:
"""Value below zero is ignored""" """Value below zero is ignored"""
@ -37,19 +43,19 @@ class Histogram:
self._sum += value self._sum += value
@property @property
def quartiles(self): def quartiles(self) -> List[int]:
return list(self._quartiles) return list(self._quartiles)
@property @property
def count(self): def count(self) -> int:
return self._count return self._count
@property @property
def sum(self): def sum(self) -> int:
return self._sum return self._sum
@property @property
def average(self): def average(self) -> float:
with self._lock: with self._lock:
if self._count != 0: if self._count != 0:
return self._sum / self._count return self._sum / self._count
@ -57,31 +63,20 @@ class Histogram:
return 0 return 0
@property @property
def quartile_percentage(self): def quartile_percentages(self) -> List[int]:
'''Quartile in percentage''' """Quartile in percentage"""
with self._lock: with self._lock:
if self._count > 0: if self._count > 0:
return [int(q * 100 / self._count) for q in self._quartiles] return [int(q * 100 / self._count) for q in self._quartiles]
else: else:
return self._quartiles return self._quartiles
@property def percentile(self, percentage: Number) -> Optional[decimal.Decimal]:
def quartile_percentage_map(self): """
result = {} Return the percentile.
# 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)
if yp != 0:
result[round(float(x), width_exponent)] = yp
x += width
return result
def percentage(self, percentage): * percentage from 0 to 100
"""
# use Decimal to avoid rounding errors # use Decimal to avoid rounding errors
x = decimal.Decimal(0) x = decimal.Decimal(0)
width = decimal.Decimal(self._width) width = decimal.Decimal(self._width)
@ -109,15 +104,21 @@ class HistogramStorage:
self.histogram_class = histogram_class self.histogram_class = histogram_class
def clear(self): def clear(self):
self.measures = {} self.measures: Dict[Tuple[str], Histogram] = {}
def configure(self, width, size, *args): def configure(self, width, size, *args):
measure = self.histogram_class(width, size) measure = self.histogram_class(width, size)
self.measures[args] = measure self.measures[args] = measure
return measure return measure
def get(self, *args): def get(self, *args, raise_on_not_found=True) -> Optional[Histogram]:
return self.measures.get(args, None) h = self.measures.get(args, None)
if raise_on_not_found and h is None:
raise ValueError("histogram " + repr((*args,)) + " doesn't not exist")
return h
def observe(self, duration, *args):
self.get(*args).observe(duration)
def dump(self): def dump(self):
logger.debug("Histograms:") logger.debug("Histograms:")
@ -136,15 +137,18 @@ class CounterStorage:
def clear(self): def clear(self):
with self.lock: with self.lock:
self.counters = {} self.counters: Dict[Tuple[str], int] = {}
def configure(self, *args): def configure(self, *args):
with self.lock: with self.lock:
self.counters[args] = 0 self.counters[args] = 0
def get(self, *args): def get(self, *args) -> int:
return self.counters[args] return self.counters[args]
def inc(self, *args):
self.add(1, *args)
def add(self, value, *args): def add(self, value, *args):
with self.lock: with self.lock:
self.counters[args] += value self.counters[args] += value

View file

@ -8,8 +8,7 @@ from urllib.parse import urlparse, unquote
from searx import logger from searx import logger
from searx import utils from searx import utils
from searx.engines import engines from searx.engines import engines
from searx.metrics import histogram_observe, counter_add, count_error from searx import metrics
CONTENT_LEN_IGNORED_CHARS_REGEX = re.compile(r'[,;:!?\./\\\\ ()-_]', re.M | re.U) CONTENT_LEN_IGNORED_CHARS_REGEX = re.compile(r'[,;:!?\./\\\\ ()-_]', re.M | re.U)
WHITESPACE_REGEX = re.compile('( |\t|\n)+', re.M | re.U) WHITESPACE_REGEX = re.compile('( |\t|\n)+', re.M | re.U)
@ -227,10 +226,10 @@ class ResultContainer:
if len(error_msgs) > 0: if len(error_msgs) > 0:
for msg in error_msgs: for msg in error_msgs:
count_error(engine_name, 'some results are invalids: ' + msg, secondary=True) metrics.count_error(engine_name, 'some results are invalids: ' + msg, secondary=True)
if engine_name in engines: if engine_name in engines:
histogram_observe(standard_result_count, 'engine', engine_name, 'result', 'count') metrics.HISTOGRAM_STORAGE.observe(standard_result_count, 'engine', engine_name, 'result', 'count')
if not self.paging and engine_name in engines and engines[engine_name].paging: if not self.paging and engine_name in engines and engines[engine_name].paging:
self.paging = True self.paging = True
@ -362,7 +361,7 @@ class ResultContainer:
result['title'] = ' '.join(utils.html_to_text(result['title']).strip().split()) result['title'] = ' '.join(utils.html_to_text(result['title']).strip().split())
for result_engine in result['engines']: for result_engine in result['engines']:
counter_add(score, 'engine', result_engine, 'score') metrics.COUNTER_STORAGE.add(score, 'engine', result_engine, 'score')
results = sorted(self._merged_results, key=itemgetter('score'), reverse=True) results = sorted(self._merged_results, key=itemgetter('score'), reverse=True)

View file

@ -20,7 +20,7 @@ from searx.plugins import plugins
from searx.search.models import EngineRef, SearchQuery from searx.search.models import EngineRef, SearchQuery
from searx.engines import load_engines from searx.engines import load_engines
from searx.network import initialize as initialize_network, check_network_configuration from searx.network import initialize as initialize_network, check_network_configuration
from searx.metrics import initialize as initialize_metrics, counter_inc, histogram_observe_time from searx import metrics
from searx.search.processors import PROCESSORS, initialize as initialize_processors from searx.search.processors import PROCESSORS, initialize as initialize_processors
from searx.search.checker import initialize as initialize_checker from searx.search.checker import initialize as initialize_checker
@ -34,7 +34,7 @@ def initialize(settings_engines=None, enable_checker=False, check_network=False,
initialize_network(settings_engines, settings['outgoing']) initialize_network(settings_engines, settings['outgoing'])
if check_network: if check_network:
check_network_configuration() check_network_configuration()
initialize_metrics([engine['name'] for engine in settings_engines], enable_metrics) metrics.initialize([engine['name'] for engine in settings_engines], enable_metrics)
initialize_processors(settings_engines) initialize_processors(settings_engines)
if enable_checker: if enable_checker:
initialize_checker() initialize_checker()
@ -102,7 +102,7 @@ class Search:
if request_params is None: if request_params is None:
continue continue
counter_inc('engine', engineref.name, 'search', 'count', 'sent') metrics.COUNTER_STORAGE.inc('engine', engineref.name, 'search', 'count', 'sent')
# append request to list # append request to list
requests.append((engineref.name, self.search_query.query, request_params)) requests.append((engineref.name, self.search_query.query, request_params))

View file

@ -17,7 +17,7 @@ from searx.utils import gen_useragent, detect_language
from searx.results import ResultContainer from searx.results import ResultContainer
from searx.search.models import SearchQuery, EngineRef from searx.search.models import SearchQuery, EngineRef
from searx.search.processors import EngineProcessor from searx.search.processors import EngineProcessor
from searx.metrics import counter_inc from searx import metrics
logger = logger.getChild('searx.search.checker') logger = logger.getChild('searx.search.checker')
@ -408,7 +408,7 @@ class Checker:
engineref_category = search_query.engineref_list[0].category engineref_category = search_query.engineref_list[0].category
params = self.processor.get_params(search_query, engineref_category) params = self.processor.get_params(search_query, engineref_category)
if params is not None: if params is not None:
counter_inc('engine', search_query.engineref_list[0].name, 'search', 'count', 'sent') metrics.COUNTER_STORAGE.inc('engine', search_query.engineref_list[0].name, 'search', 'count', 'sent')
self.processor.search(search_query.query, params, result_container, default_timer(), 5) self.processor.search(search_query.query, params, result_container, default_timer(), 5)
return result_container return result_container

View file

@ -13,7 +13,7 @@ from typing import Dict, Union
from searx import settings, logger from searx import settings, logger
from searx.engines import engines from searx.engines import engines
from searx.network import get_time_for_thread, get_network from searx.network import get_time_for_thread, get_network
from searx.metrics import histogram_observe, counter_inc, count_exception, count_error from searx import metrics
from searx.exceptions import SearxEngineAccessDeniedException, SearxEngineResponseException from searx.exceptions import SearxEngineAccessDeniedException, SearxEngineResponseException
from searx.utils import get_engine_from_settings from searx.utils import get_engine_from_settings
@ -95,11 +95,11 @@ class EngineProcessor(ABC):
error_message = exception_or_message error_message = exception_or_message
result_container.add_unresponsive_engine(self.engine_name, error_message) result_container.add_unresponsive_engine(self.engine_name, error_message)
# metrics # metrics
counter_inc('engine', self.engine_name, 'search', 'count', 'error') metrics.COUNTER_STORAGE.inc('engine', self.engine_name, 'search', 'count', 'error')
if isinstance(exception_or_message, BaseException): if isinstance(exception_or_message, BaseException):
count_exception(self.engine_name, exception_or_message) metrics.count_exception(self.engine_name, exception_or_message)
else: else:
count_error(self.engine_name, exception_or_message) metrics.count_error(self.engine_name, exception_or_message)
# suspend the engine ? # suspend the engine ?
if suspend: if suspend:
suspended_time = None suspended_time = None
@ -114,10 +114,10 @@ class EngineProcessor(ABC):
page_load_time = get_time_for_thread() page_load_time = get_time_for_thread()
result_container.add_timing(self.engine_name, engine_time, page_load_time) result_container.add_timing(self.engine_name, engine_time, page_load_time)
# metrics # metrics
counter_inc('engine', self.engine_name, 'search', 'count', 'successful') metrics.COUNTER_STORAGE.inc('engine', self.engine_name, 'search', 'count', 'successful')
histogram_observe(engine_time, 'engine', self.engine_name, 'time', 'total') metrics.HISTOGRAM_STORAGE.observe(engine_time, 'engine', self.engine_name, 'time', 'total')
if page_load_time is not None: if page_load_time is not None:
histogram_observe(page_load_time, 'engine', self.engine_name, 'time', 'http') metrics.HISTOGRAM_STORAGE.observe(page_load_time, 'engine', self.engine_name, 'time', 'http')
def extend_container(self, result_container, start_time, search_results): def extend_container(self, result_container, start_time, search_results):
if getattr(threading.current_thread(), '_timeout', False): if getattr(threading.current_thread(), '_timeout', False):

View file

@ -18,7 +18,8 @@ from searx.exceptions import (
SearxEngineCaptchaException, SearxEngineCaptchaException,
SearxEngineTooManyRequestsException, SearxEngineTooManyRequestsException,
) )
from searx.metrics.error_recorder import count_error
from searx import metrics
from .abstract import EngineProcessor from .abstract import EngineProcessor
@ -124,7 +125,7 @@ class OnlineProcessor(EngineProcessor):
status_code = str(response.status_code or '') status_code = str(response.status_code or '')
reason = response.reason_phrase or '' reason = response.reason_phrase or ''
hostname = response.url.host hostname = response.url.host
count_error( metrics.count_error(
self.engine_name, self.engine_name,
'{} redirects, maximum: {}'.format(len(response.history), soft_max_redirects), '{} redirects, maximum: {}'.format(len(response.history), soft_max_redirects),
(status_code, reason, hostname), (status_code, reason, hostname),

View file

@ -106,13 +106,8 @@ from searx.answerers import (
answerers, answerers,
ask, ask,
) )
from searx.metrics import ( from searx import metrics
get_engines_stats,
get_engine_errors,
get_reliabilities,
histogram,
counter,
)
from searx.flaskfix import patch_application from searx.flaskfix import patch_application
from searx.locales import ( from searx.locales import (
@ -910,15 +905,15 @@ def preferences():
stats = {} # pylint: disable=redefined-outer-name stats = {} # pylint: disable=redefined-outer-name
max_rate95 = 0 max_rate95 = 0
for _, e in filtered_engines.items(): for _, e in filtered_engines.items():
h = histogram('engine', e.name, 'time', 'total') h = metrics.HISTOGRAM_STORAGE.get('engine', e.name, 'time', 'total')
median = round(h.percentage(50), 1) if h.count > 0 else None median = round(h.percentile(50), 1) if h.count > 0 else None # type: ignore
rate80 = round(h.percentage(80), 1) if h.count > 0 else None rate80 = round(h.percentile(80), 1) if h.count > 0 else None # type: ignore
rate95 = round(h.percentage(95), 1) if h.count > 0 else None rate95 = round(h.percentile(95), 1) if h.count > 0 else None # type: ignore
max_rate95 = max(max_rate95, rate95 or 0) max_rate95 = max(max_rate95, rate95 or 0)
result_count_sum = histogram('engine', e.name, 'result', 'count').sum result_count_sum = metrics.HISTOGRAM_STORAGE.get('engine', e.name, 'result', 'count').sum
successful_count = counter('engine', e.name, 'search', 'count', 'successful') successful_count = metrics.COUNTER_STORAGE.get('engine', e.name, 'search', 'count', 'successful')
result_count = int(result_count_sum / float(successful_count)) if successful_count else 0 result_count = int(result_count_sum / float(successful_count)) if successful_count else 0
stats[e.name] = { stats[e.name] = {
@ -935,7 +930,7 @@ def preferences():
# reliabilities # reliabilities
reliabilities = {} reliabilities = {}
engine_errors = get_engine_errors(filtered_engines) engine_errors = metrics.get_engine_errors(filtered_engines)
checker_results = checker_get_result() checker_results = checker_get_result()
checker_results = ( checker_results = (
checker_results['engines'] if checker_results['status'] == 'ok' and 'engines' in checker_results else {} checker_results['engines'] if checker_results['status'] == 'ok' and 'engines' in checker_results else {}
@ -944,7 +939,7 @@ def preferences():
checker_result = checker_results.get(e.name, {}) checker_result = checker_results.get(e.name, {})
checker_success = checker_result.get('success', True) checker_success = checker_result.get('success', True)
errors = engine_errors.get(e.name) or [] errors = engine_errors.get(e.name) or []
if counter('engine', e.name, 'search', 'count', 'sent') == 0: if metrics.COUNTER_STORAGE.get('engine', e.name, 'search', 'count', 'sent') == 0:
# no request # no request
reliability = None reliability = None
elif checker_success and not errors: elif checker_success and not errors:
@ -1143,8 +1138,8 @@ def stats():
checker_results['engines'] if checker_results['status'] == 'ok' and 'engines' in checker_results else {} checker_results['engines'] if checker_results['status'] == 'ok' and 'engines' in checker_results else {}
) )
engine_stats = get_engines_stats(filtered_engines) engine_metrics = metrics.get_engines_metrics(filtered_engines)
engine_reliabilities = get_reliabilities(filtered_engines, checker_results) engine_reliabilities = metrics.get_reliabilities(filtered_engines, checker_results)
if sort_order not in STATS_SORT_PARAMETERS: if sort_order not in STATS_SORT_PARAMETERS:
sort_order = 'name' sort_order = 'name'
@ -1163,12 +1158,12 @@ def stats():
reliability_order = 1 - reliability_order reliability_order = 1 - reliability_order
return (reliability_order, key, engine_stat['name']) return (reliability_order, key, engine_stat['name'])
engine_stats['time'] = sorted(engine_stats['time'], reverse=reverse, key=get_key) engine_metrics['time'] = sorted(engine_metrics['time'], reverse=reverse, key=get_key)
return render( return render(
# fmt: off # fmt: off
'stats.html', 'stats.html',
sort_order = sort_order, sort_order = sort_order,
engine_stats = engine_stats, engine_stats = engine_metrics,
engine_reliabilities = engine_reliabilities, engine_reliabilities = engine_reliabilities,
selected_engine_name = selected_engine_name, selected_engine_name = selected_engine_name,
searx_git_branch = GIT_BRANCH, searx_git_branch = GIT_BRANCH,
@ -1179,7 +1174,7 @@ def stats():
@app.route('/stats/errors', methods=['GET']) @app.route('/stats/errors', methods=['GET'])
def stats_errors(): def stats_errors():
filtered_engines = dict(filter(lambda kv: request.preferences.validate_token(kv[1]), engines.items())) filtered_engines = dict(filter(lambda kv: request.preferences.validate_token(kv[1]), engines.items()))
result = get_engine_errors(filtered_engines) result = metrics.get_engine_errors(filtered_engines)
return jsonify(result) return jsonify(result)