Merge pull request #12 from searxng/metrics-stats-page

[mod] update /stats
This commit is contained in:
Alexandre Flament 2021-04-24 07:02:52 +02:00 committed by GitHub
commit 3cdd6a6a50
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 476 additions and 178 deletions

View File

@ -199,7 +199,8 @@ PYLINT_FILES=\
searx/engines/yahoo_news.py \
searx/engines/apkmirror.py \
searx/engines/artic.py \
searx_extra/update/update_external_bangs.py
searx_extra/update/update_external_bangs.py \
searx/metrics/__init__.py
test.pylint: pyenvinstall
$(call cmd,pylint,$(PYLINT_FILES))

View File

@ -1,4 +1,5 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
# pylint: disable=missing-module-docstring, missing-function-docstring
import typing
import math
@ -63,7 +64,7 @@ def initialize(engine_names=None):
"""
Initialize metrics
"""
global counter_storage, histogram_storage
global counter_storage, histogram_storage # pylint: disable=global-statement
counter_storage = CounterStorage()
histogram_storage = HistogramStorage()
@ -96,12 +97,12 @@ def initialize(engine_names=None):
histogram_storage.configure(histogram_width, histogram_size, 'engine', engine_name, 'time', 'total')
def get_engine_errors(engline_list):
def get_engine_errors(engline_name_list):
result = {}
engine_names = list(errors_per_engines.keys())
engine_names.sort()
for engine_name in engine_names:
if engine_name not in engline_list:
if engine_name not in engline_name_list:
continue
error_stats = errors_per_engines[engine_name]
@ -125,82 +126,88 @@ def get_engine_errors(engline_list):
return result
def to_percentage(stats, maxvalue):
for engine_stat in stats:
if maxvalue:
engine_stat['percentage'] = int(engine_stat['avg'] / maxvalue * 100)
def get_reliabilities(engline_name_list, checker_results):
reliabilities = {}
engine_errors = get_engine_errors(engline_name_list)
for engine_name in engline_name_list:
checker_result = checker_results.get(engine_name, {})
checker_success = checker_result.get('success', True)
errors = engine_errors.get(engine_name) or []
if counter('engine', engine_name, 'search', 'count', 'sent') == 0:
# no request
reliablity = None
elif checker_success and not errors:
reliablity = 100
elif 'simple' in checker_result.get('errors', {}):
# the basic (simple) test doesn't work: the engine is broken accoding to the checker
# even if there is no exception
reliablity = 0
else:
engine_stat['percentage'] = 0
return stats
reliablity = 100 - sum([error['percentage'] for error in errors if not error.get('secondary')])
reliabilities[engine_name] = {
'reliablity': reliablity,
'errors': errors,
'checker': checker_results.get(engine_name, {}).get('errors', {}).keys(),
}
return reliabilities
def get_engines_stats(engine_list):
global counter_storage, histogram_storage
def round_or_none(number, digits):
return round(number, digits) if number else number
def get_engines_stats(engine_name_list):
assert counter_storage is not None
assert histogram_storage is not None
list_time = []
list_time_http = []
list_time_total = []
list_result_count = []
list_error_count = []
list_scores = []
list_scores_per_result = []
max_error_count = max_http_time = max_time_total = max_result_count = max_score = None # noqa
for engine_name in engine_list:
error_count = counter('engine', engine_name, 'search', 'count', 'error')
if counter('engine', engine_name, 'search', 'count', 'sent') > 0:
list_error_count.append({'avg': error_count, 'name': engine_name})
max_error_count = max(error_count, max_error_count or 0)
successful_count = counter('engine', engine_name, 'search', 'count', 'successful')
if successful_count == 0:
max_time_total = max_result_count = None # noqa
for engine_name in engine_name_list:
sent_count = counter('engine', engine_name, 'search', 'count', 'sent')
if sent_count == 0:
continue
result_count_sum = histogram('engine', engine_name, 'result', 'count').sum
successful_count = counter('engine', engine_name, 'search', 'count', 'successful')
time_total = histogram('engine', engine_name, 'time', 'total').percentage(50)
time_http = histogram('engine', engine_name, 'time', 'http').percentage(50)
result_count = result_count_sum / float(successful_count)
time_total_p80 = histogram('engine', engine_name, 'time', 'total').percentage(80)
time_http_p80 = histogram('engine', engine_name, 'time', 'http').percentage(80)
time_total_p95 = histogram('engine', engine_name, 'time', 'total').percentage(95)
time_http_p95 = histogram('engine', engine_name, 'time', 'http').percentage(95)
if result_count:
result_count = histogram('engine', engine_name, 'result', 'count').percentage(50)
result_count_sum = histogram('engine', engine_name, 'result', 'count').sum
if successful_count and result_count_sum:
score = counter('engine', engine_name, 'score') # noqa
score_per_result = score / float(result_count_sum)
else:
score = score_per_result = 0.0
max_time_total = max(time_total, max_time_total or 0)
max_http_time = max(time_http, max_http_time or 0)
max_result_count = max(result_count, max_result_count or 0)
max_score = max(score, max_score or 0)
list_time.append({'total': round(time_total, 1),
'http': round(time_http, 1),
'name': engine_name,
'processing': round(time_total - time_http, 1)})
list_time_total.append({'avg': time_total, 'name': engine_name})
list_time_http.append({'avg': time_http, 'name': engine_name})
list_result_count.append({'avg': result_count, 'name': engine_name})
list_scores.append({'avg': score, 'name': engine_name})
list_scores_per_result.append({'avg': score_per_result, 'name': engine_name})
list_time = sorted(list_time, key=itemgetter('total'))
list_time_total = sorted(to_percentage(list_time_total, max_time_total), key=itemgetter('avg'))
list_time_http = sorted(to_percentage(list_time_http, max_http_time), key=itemgetter('avg'))
list_result_count = sorted(to_percentage(list_result_count, max_result_count), key=itemgetter('avg'), reverse=True)
list_scores = sorted(list_scores, key=itemgetter('avg'), reverse=True)
list_scores_per_result = sorted(list_scores_per_result, key=itemgetter('avg'), reverse=True)
list_error_count = sorted(to_percentage(list_error_count, max_error_count), key=itemgetter('avg'), reverse=True)
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)
list_time.append({
'name': engine_name,
'total': round_or_none(time_total, 1),
'total_p80': round_or_none(time_total_p80, 1),
'total_p95': round_or_none(time_total_p95, 1),
'http': round_or_none(time_http, 1),
'http_p80': round_or_none(time_http_p80, 1),
'http_p95': round_or_none(time_http_p95, 1),
'processing': round(time_total - time_http, 1) if time_total else None,
'processing_p80': round(time_total_p80 - time_http_p80, 1) if time_total else None,
'processing_p95': round(time_total_p95 - time_http_p95, 1) if time_total else None,
'score': score,
'score_per_result': score_per_result,
'result_count': result_count,
})
return {
'time': list_time,
'max_time': math.ceil(max_time_total or 0),
'time_total': list_time_total,
'time_http': list_time_http,
'result_count': list_result_count,
'scores': list_scores,
'scores_per_result': list_scores_per_result,
'error_count': list_error_count,
'max_result_count': math.ceil(max_result_count or 0),
}

View File

@ -998,3 +998,21 @@ th:hover .engine-tooltip,
padding: 0.4rem 0;
width: 1px;
}
.stacked-bar-chart-serie1 {
display: flex;
flex-shrink: 0;
flex-grow: 0;
flex-basis: unset;
background: #5bc0de;
box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15);
padding: 0.4rem 0;
}
.stacked-bar-chart-serie2 {
display: flex;
flex-shrink: 0;
flex-grow: 0;
flex-basis: unset;
background: #deb15b;
box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15);
padding: 0.4rem 0;
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -971,6 +971,24 @@ th:hover .engine-tooltip,
padding: 0.4rem 0;
width: 1px;
}
.stacked-bar-chart-serie1 {
display: flex;
flex-shrink: 0;
flex-grow: 0;
flex-basis: unset;
background: #5bc0de;
box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15);
padding: 0.4rem 0;
}
.stacked-bar-chart-serie2 {
display: flex;
flex-shrink: 0;
flex-grow: 0;
flex-basis: unset;
background: #deb15b;
box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15);
padding: 0.4rem 0;
}
/*Global*/
body {
background: #1d1f21 none !important;

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -682,6 +682,7 @@ input[type=checkbox]:not(:checked) + .label_hide_if_checked + .label_hide_if_not
padding: 0.5rem 1rem;
margin: 0rem 0 0 2rem;
border: 1px solid #ddd;
box-shadow: 2px 2px 2px 0px rgba(0, 0, 0, 0.1);
background: white;
font-size: 14px;
font-weight: normal;
@ -756,3 +757,21 @@ td:hover .engine-tooltip,
padding: 0.4rem 0;
width: 1px;
}
.stacked-bar-chart-serie1 {
display: flex;
flex-shrink: 0;
flex-grow: 0;
flex-basis: unset;
background: #5bc0de;
box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15);
padding: 0.4rem 0;
}
.stacked-bar-chart-serie2 {
display: flex;
flex-shrink: 0;
flex-grow: 0;
flex-basis: unset;
background: #deb15b;
box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15);
padding: 0.4rem 0;
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -89,3 +89,17 @@ td:hover .engine-tooltip, th:hover .engine-tooltip, .engine-tooltip:hover {
padding: 0.4rem 0;
width: 1px;
}
.stacked-bar-chart-serie1 {
.stacked-bar-chart-base();
background: #5bc0de;
box-shadow: inset 0 -1px 0 rgba(0,0,0,.15);
padding: 0.4rem 0;
}
.stacked-bar-chart-serie2 {
.stacked-bar-chart-base();
background: #deb15b;
box-shadow: inset 0 -1px 0 rgba(0,0,0,.15);
padding: 0.4rem 0;
}

View File

@ -8,6 +8,7 @@
padding: 0.5rem 1rem;
margin: 0rem 0 0 2rem;
border: 1px solid #ddd;
box-shadow: 2px 2px 2px 0px rgba(0,0,0,0.1);
background: white;
font-size: 14px;
font-weight: normal;
@ -77,3 +78,17 @@ th:hover .engine-tooltip, td:hover .engine-tooltip, .engine-tooltip:hover {
width: 1px;
}
.stacked-bar-chart-serie1 {
.stacked-bar-chart-base();
background: #5bc0de;
box-shadow: inset 0 -1px 0 rgba(0,0,0,.15);
padding: 0.4rem 0;
}
.stacked-bar-chart-serie2 {
.stacked-bar-chart-base();
background: #deb15b;
box-shadow: inset 0 -1px 0 rgba(0,0,0,.15);
padding: 0.4rem 0;
}

View File

@ -1,4 +1,4 @@
/*! searx | 21-04-2021 | */
/*! searx | 23-04-2021 | */
/*
* searx, A privacy-respecting, hackable metasearch engine
*
@ -1153,6 +1153,25 @@ select:focus {
transform: rotate(360deg);
}
}
/* -- engine-tooltip -- */
.engine-tooltip {
display: none;
position: absolute;
padding: 0.5rem 1rem;
margin: 0rem 0 0 2rem;
border: 1px solid #ddd;
box-shadow: 2px 2px 2px 0px rgba(0, 0, 0, 0.1);
background: white;
font-size: 14px;
font-weight: normal;
z-index: 1000000;
text-align: left;
}
th:hover .engine-tooltip,
td:hover .engine-tooltip,
.engine-tooltip:hover {
display: inline-block;
}
/* -- stacked bar chart -- */
.stacked-bar-chart {
margin: 0;
@ -1216,6 +1235,24 @@ select:focus {
padding: 0.4rem 0;
width: 1px;
}
.stacked-bar-chart-serie1 {
display: flex;
flex-shrink: 0;
flex-grow: 0;
flex-basis: unset;
background: #5bc0de;
box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15);
padding: 0.4rem 0;
}
.stacked-bar-chart-serie2 {
display: flex;
flex-shrink: 0;
flex-grow: 0;
flex-basis: unset;
background: #deb15b;
box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15);
padding: 0.4rem 0;
}
/*! Autocomplete.js v2.6.3 | license MIT | (c) 2017, Baptiste Donaux | http://autocomplete-js.com */
.autocomplete {
position: absolute;
@ -1494,23 +1531,6 @@ select:focus {
#main_preferences div.selectable_url pre {
width: 100%;
}
#main_preferences .engine-tooltip {
display: none;
position: absolute;
padding: 0.5rem 1rem;
margin: 0rem 0 0 2rem;
border: 1px solid #ddd;
background: white;
font-size: 14px;
font-weight: normal;
z-index: 1000000;
text-align: left;
}
#main_preferences th:hover .engine-tooltip,
#main_preferences td:hover .engine-tooltip,
#main_preferences .engine-tooltip:hover {
display: inline-block;
}
@media screen and (max-width: 75em) {
.preferences_back {
clear: both;

File diff suppressed because one or more lines are too long

View File

@ -1,4 +1,4 @@
/*! searx | 21-04-2021 | */
/*! searx | 23-04-2021 | */
/*
* searx, A privacy-respecting, hackable metasearch engine
*
@ -1153,6 +1153,25 @@ select:focus {
transform: rotate(360deg);
}
}
/* -- engine-tooltip -- */
.engine-tooltip {
display: none;
position: absolute;
padding: 0.5rem 1rem;
margin: 0rem 0 0 2rem;
border: 1px solid #ddd;
box-shadow: 2px 2px 2px 0px rgba(0, 0, 0, 0.1);
background: white;
font-size: 14px;
font-weight: normal;
z-index: 1000000;
text-align: left;
}
th:hover .engine-tooltip,
td:hover .engine-tooltip,
.engine-tooltip:hover {
display: inline-block;
}
/* -- stacked bar chart -- */
.stacked-bar-chart {
margin: 0;
@ -1216,6 +1235,24 @@ select:focus {
padding: 0.4rem 0;
width: 1px;
}
.stacked-bar-chart-serie1 {
display: flex;
flex-shrink: 0;
flex-grow: 0;
flex-basis: unset;
background: #5bc0de;
box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15);
padding: 0.4rem 0;
}
.stacked-bar-chart-serie2 {
display: flex;
flex-shrink: 0;
flex-grow: 0;
flex-basis: unset;
background: #deb15b;
box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15);
padding: 0.4rem 0;
}
/*! Autocomplete.js v2.6.3 | license MIT | (c) 2017, Baptiste Donaux | http://autocomplete-js.com */
.autocomplete {
position: absolute;
@ -1494,23 +1531,6 @@ select:focus {
#main_preferences div.selectable_url pre {
width: 100%;
}
#main_preferences .engine-tooltip {
display: none;
position: absolute;
padding: 0.5rem 1rem;
margin: 0rem 0 0 2rem;
border: 1px solid #ddd;
background: white;
font-size: 14px;
font-weight: normal;
z-index: 1000000;
text-align: left;
}
#main_preferences th:hover .engine-tooltip,
#main_preferences td:hover .engine-tooltip,
#main_preferences .engine-tooltip:hover {
display: inline-block;
}
@media screen and (max-width: 75em) {
.preferences_back {
clear: both;

File diff suppressed because one or more lines are too long

View File

@ -1,4 +1,4 @@
/*! simple/searx.min.js | 21-04-2021 | */
/*! simple/searx.min.js | 23-04-2021 | */
(function(t,e){"use strict";var a=e.currentScript||function(){var t=e.getElementsByTagName("script");return t[t.length-1]}();t.searx={touch:"ontouchstart"in t||t.DocumentTouch&&document instanceof DocumentTouch||false,method:a.getAttribute("data-method"),autocompleter:a.getAttribute("data-autocompleter")==="true",search_on_category_select:a.getAttribute("data-search-on-category-select")==="true",infinite_scroll:a.getAttribute("data-infinite-scroll")==="true",static_path:a.getAttribute("data-static-path"),translations:JSON.parse(a.getAttribute("data-translations"))};e.getElementsByTagName("html")[0].className=t.searx.touch?"js touch":"js"})(window,document);
//# sourceMappingURL=searx.head.min.js.map

View File

@ -1,4 +1,4 @@
/*! simple/searx.min.js | 21-04-2021 | */
/*! simple/searx.min.js | 23-04-2021 | */
window.searx=function(t,a){"use strict";if(t.Element){(function(e){e.matches=e.matches||e.matchesSelector||e.webkitMatchesSelector||e.msMatchesSelector||function(e){var t=this,n=(t.parentNode||t.document).querySelectorAll(e),i=-1;while(n[++i]&&n[i]!=t);return!!n[i]}})(Element.prototype)}function o(e,t,n){try{e.call(t,n)}catch(e){console.log(e)}}var s=window.searx||{};s.on=function(i,e,r,t){t=t||false;if(typeof i!=="string"){i.addEventListener(e,r,t)}else{a.addEventListener(e,function(e){var t=e.target||e.srcElement,n=false;while(t&&t.matches&&t!==a&&!(n=t.matches(i)))t=t.parentElement;if(n)o(r,t,e)},t)}};s.ready=function(e){if(document.readyState!="loading"){e.call(t)}else{t.addEventListener("DOMContentLoaded",e.bind(t))}};s.http=function(e,t,n){var i=new XMLHttpRequest,r=function(){},a=function(){},o={then:function(e){r=e;return o},catch:function(e){a=e;return o}};try{i.open(e,t,true);i.onload=function(){if(i.status==200){r(i.response,i.responseType)}else{a(Error(i.statusText))}};i.onerror=function(){a(Error("Network Error"))};i.onabort=function(){a(Error("Transaction is aborted"))};i.send()}catch(e){a(e)}return o};s.loadStyle=function(e){var t=s.static_path+e,n="style_"+e.replace(".","_"),i=a.getElementById(n);if(i===null){i=a.createElement("link");i.setAttribute("id",n);i.setAttribute("rel","stylesheet");i.setAttribute("type","text/css");i.setAttribute("href",t);a.body.appendChild(i)}};s.loadScript=function(e,t){var n=s.static_path+e,i="script_"+e.replace(".","_"),r=a.getElementById(i);if(r===null){r=a.createElement("script");r.setAttribute("id",i);r.setAttribute("src",n);r.onload=t;r.onerror=function(){r.setAttribute("error","1")};a.body.appendChild(r)}else if(!r.hasAttribute("error")){try{t.apply(r,[])}catch(e){console.log(e)}}else{console.log("callback not executed : script '"+n+"' not loaded.")}};s.insertBefore=function(e,t){element.parentNode.insertBefore(e,t)};s.insertAfter=function(e,t){t.parentNode.insertBefore(e,t.nextSibling)};s.on(".close","click",function(e){var t=e.target||e.srcElement;this.parentNode.classList.add("invisible")});return s}(window,document);(function(e){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=e()}else if(typeof define==="function"&&define.amd){define([],e)}else{var t;if(typeof window!=="undefined"){t=window}else if(typeof global!=="undefined"){t=global}else if(typeof self!=="undefined"){t=self}else{t=this}t.AutoComplete=e()}})(function(){var e,t,n;return function a(o,s,l){function u(n,e){if(!s[n]){if(!o[n]){var t=typeof require=="function"&&require;if(!e&&t)return t(n,!0);if(c)return c(n,!0);var i=new Error("Cannot find module '"+n+"'");throw i.code="MODULE_NOT_FOUND",i}var r=s[n]={exports:{}};o[n][0].call(r.exports,function(e){var t=o[n][1][e];return u(t?t:e)},r,r.exports,a,o,s,l)}return s[n].exports}var c=typeof require=="function"&&require;for(var e=0;e<l.length;e++)u(l[e]);return u}({1:[function(e,t,n){
/*

View File

@ -94,24 +94,6 @@
}
}
.engine-tooltip {
display: none;
position: absolute;
padding: 0.5rem 1rem;
margin: 0rem 0 0 2rem;
border: 1px solid #ddd;
background: white;
font-size: 14px;
font-weight: normal;
z-index: 1000000;
text-align: left;
}
th:hover .engine-tooltip, td:hover .engine-tooltip, .engine-tooltip:hover {
display: inline-block;
}
}
@media screen and (max-width: 75em) {

View File

@ -475,6 +475,25 @@ select {
}
}
/* -- engine-tooltip -- */
.engine-tooltip {
display: none;
position: absolute;
padding: 0.5rem 1rem;
margin: 0rem 0 0 2rem;
border: 1px solid #ddd;
box-shadow: 2px 2px 2px 0px rgba(0,0,0,0.1);
background: white;
font-size: 14px;
font-weight: normal;
z-index: 1000000;
text-align: left;
}
th:hover .engine-tooltip, td:hover .engine-tooltip, .engine-tooltip:hover {
display: inline-block;
}
/* -- stacked bar chart -- */
.stacked-bar-chart {
margin: 0;
@ -532,3 +551,17 @@ select {
padding: 0.4rem 0;
width: 1px;
}
.stacked-bar-chart-serie1 {
.stacked-bar-chart-base();
background: #5bc0de;
box-shadow: inset 0 -1px 0 rgba(0,0,0,.15);
padding: 0.4rem 0;
}
.stacked-bar-chart-serie2 {
.stacked-bar-chart-base();
background: #deb15b;
box-shadow: inset 0 -1px 0 rgba(0,0,0,.15);
padding: 0.4rem 0;
}

View File

@ -1,45 +1,97 @@
{% extends "oscar/base.html" %}
{% block styles %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/charts.min.css') }}" type="text/css" />
<style>
#engine-times {
--labels-size: 20rem;
}
#engine-times th {
text-align: right;
}
</style>
{% endblock %}
{% block title %}{{ _('stats') }} - {% endblock %}
{%- macro th_sort(column_order, column_name) -%}
{% if column_order==sort_order %}
{{ column_name }} {{ icon('chevron-down') }}
{% else %}
<a href="{{ url_for('stats', sort=column_order) }}">{{ column_name }}
{% endif %}
{%- endmacro -%}
{% block content %}
<div class="container-fluid">
<h1>{{ _('Engine stats') }}</h1>
<div class="row">
{% for stat_name,stat_category in stats %}
<div class="col-xs-12 col-sm-12 col-md-6">
<h3>{{ stat_name }}</h3>
<div class="container-fluid">
{% for engine in stat_category %}
<div class="row">
<div class="col-sm-4 col-md-4">{{ engine.name }}</div>
<div class="col-sm-8 col-md-8">
<div class="progress">
<div class="progress-bar progress-bar-info" role="progressbar" aria-valuenow="{{ '%i'|format(engine.avg) }}" aria-valuemin="0" aria-valuemax="100" style="width: {{ engine.percentage }}%;">
{{ '%.02f'|format(engine.avg) }}
</div>
</div>
<div class="col-xs-12 col-sm-12 col-md-12">
<div class="table-responsive">
{% if not engine_stats.get('time') %}
<div class="col-sm-12 col-md-12">
{% include 'oscar/messages/no_data_available.html' %}
</div>
</div>
{% endfor %}
{% if not stat_category %}
<div class="col-sm-12 col-md-12">
{% include 'oscar/messages/no_data_available.html' %}
</div>
{% else %}
<table class="table table-hover table-condensed table-striped">
<tr>
<th scope="col" style="width:20rem;">{{ th_sort('name', _("Engine name")) }}</th>
<th scope="col" style="width:7rem; text-align: right;">{{ th_sort('score', _('Scores')) }}</th>
<th scope="col">{{ th_sort('result_count', _('Result count')) }}</th>
<th scope="col">{{ th_sort('time', _('Response time')) }}</th>
<th scope="col" style="text-align: right;">{{ th_sort('reliability', _('Reliability')) }}</th>
</tr>
{% for engine_stat in engine_stats.get('time', []) %}
<tr>
<td>{{ engine_stat.name }}</td>
<td style="text-align: right;">
{% if engine_stat.score %}
<span aria-labelledby="{{engine_stat.name}}_score" >{{ engine_stat.score|round(1) }}</span>
<div class="engine-tooltip text-left" role="tooltip" id="{{engine_stat.name}}_score">{{- "" -}}
<p>{{ _('Scores per result') }}: {{ engine_stat.score_per_result | round(3) }}</p>
</div>
{% endif %}
</td>
<td>
{%- if engine_stat.result_count -%}
<span class="stacked-bar-chart-value">{{- engine_stat.result_count | int -}}</span>{{- "" -}}
<span class="stacked-bar-chart" aria-hidden="true">{{- "" -}}
<span style="width: calc(max(2px, 100%*{{ (engine_stat.result_count / engine_stats.max_result_count )|round(3) }}))" class="stacked-bar-chart-serie1"></span>{{- "" -}}
</span>
{%- endif -%}
</td>
<td>
{%- if engine_stat.total -%}
<span class="stacked-bar-chart-value">{{- engine_stat.total | round(1) -}}</span>{{- "" -}}
<span class="stacked-bar-chart" aria-labelledby="{{engine_stat.name}}_time" aria-hidden="true">{{- "" -}}
<span style="width: calc(max(2px, 100%*{{ (engine_stat.http / engine_stats.max_time )|round(3) }}))" class="stacked-bar-chart-serie1"></span>{{- "" -}}
<span style="width: calc(100%*{{ engine_stat.processing / engine_stats.max_time |round(3) }})" class="stacked-bar-chart-serie2"></span>{{- "" -}}
</span>{{- "" -}}
<div class="engine-tooltip text-left" role="tooltip" id="{{engine_stat.name}}_time">{{- "" -}}
<table class="table table-striped">
<tr>
<th scope="col"></th>
<th scope="col">{{ _('Total') }}</th>
<th scope="col">{{ _('HTTP') }}</th>
<th scope="col">{{ _('Processing') }}</th>
</tr>
<tr>
<th scope="col">{{ _('Median') }}</th>
<td>{{ engine_stat.total }}</td>
<td>{{ engine_stat.http }}</td>
<td>{{ engine_stat.processing }}</td>
</tr>
<tr>
<th scope="col">{{ _('P80') }}</th>
<td>{{ engine_stat.total_p80 }}</td>
<td>{{ engine_stat.http_p80 }}</td>
<td>{{ engine_stat.processing_p80 }}</td>
</tr>
<tr>
<th scope="col">{{ _('P95') }}</th>
<td>{{ engine_stat.total_p95 }}</td>
<td>{{ engine_stat.http_p95 }}</td>
<td>{{ engine_stat.processing_p95 }}</td>
</tr>
</table>
</div>
{%- endif -%}
</td>
<td style="text-align: right;"> {{ engine_reliabilities.get(engine_stat.name, {}).get('reliablity') }}</td>
</tr>
{% endfor %}
</table>
{% endif %}
</div>
</div>
{% endfor %}
</div>
</div>
{% endblock %}

View File

@ -1,4 +1,15 @@
{% from 'simple/macros.html' import icon %}
{% extends "simple/base.html" %}
{%- macro th_sort(column_order, column_name) -%}
{% if column_order==sort_order %}
{{ column_name }} {{ icon('arrow-dropdown') }}
{% else %}
<a href="{{ url_for('stats', sort=column_order) }}">{{ column_name }}
{% endif %}
{%- endmacro -%}
{% block head %} {% endblock %}
{% block content %}
@ -6,20 +17,77 @@
<h2>{{ _('Engine stats') }}</h2>
{% for stat_name,stat_category in stats %}
<div class="left">
<table>
<tr colspan="3">
<th>{{ stat_name }}</th>
</tr>
{% for engine in stat_category %}
<tr>
<td>{{ engine.name }}</td>
<td>{{ '%.02f'|format(engine.avg) }}</td>
<td class="percentage"><div style="width: {{ engine.percentage }}%">&nbsp;</div></td>
</tr>
{% endfor %}
</table>
</div>
{% endfor %}
{% if not engine_stats.get('time') %}
{{ _('There is currently no data available. ') }}
{% else %}
<table style="max-width: 1280px; margin: 0 auto;">
<tr>
<th scope="col" style="width:20rem;">{{ th_sort('name', _("Engine name")) }}</th>
<th scope="col" style="width:7rem; text-align: right;">{{ th_sort('score', _('Scores')) }}</th>
<th scope="col">{{ th_sort('result_count', _('Result count')) }}</th>
<th scope="col">{{ th_sort('time', _('Response time')) }}</th>
<th scope="col" style="text-align: right;">{{ th_sort('reliability', _('Reliability')) }}</th>
</tr>
{% for engine_stat in engine_stats.get('time', []) %}
<tr>
<td>{{ engine_stat.name }}</td>
<td style="text-align: right;">
{% if engine_stat.score %}
<span aria-labelledby="{{engine_stat.name}}_score" >{{ engine_stat.score|round(1) }}</span>
<div class="engine-tooltip" role="tooltip" id="{{engine_stat.name}}_score">{{- "" -}}
<p>{{ _('Scores per result') }}: {{ engine_stat.score_per_result | round(3) }}</p>
</div>
{% endif %}
</td>
<td>
{%- if engine_stat.result_count -%}
<span class="stacked-bar-chart-value">{{- engine_stat.result_count | int -}}</span>{{- "" -}}
<span class="stacked-bar-chart" aria-hidden="true">{{- "" -}}
<span style="width: calc(max(2px, 100%*{{ (engine_stat.result_count / engine_stats.max_result_count )|round(3) }}))" class="stacked-bar-chart-serie1"></span>{{- "" -}}
</span>
{%- endif -%}
</td>
<td>
{%- if engine_stat.total -%}
<span class="stacked-bar-chart-value">{{- engine_stat.total | round(1) -}}</span>{{- "" -}}
<span class="stacked-bar-chart" aria-labelledby="{{engine_stat.name}}_time" aria-hidden="true">{{- "" -}}
<span style="width: calc(max(2px, 100%*{{ (engine_stat.http / engine_stats.max_time )|round(3) }}))" class="stacked-bar-chart-serie1"></span>{{- "" -}}
<span style="width: calc(100%*{{ engine_stat.processing / engine_stats.max_time |round(3) }})" class="stacked-bar-chart-serie2"></span>{{- "" -}}
</span>{{- "" -}}
<div class="engine-tooltip" role="tooltip" id="{{engine_stat.name}}_time">{{- "" -}}
<table>
<tr>
<th scope="col"></th>
<th scope="col">{{ _('Total') }}</th>
<th scope="col">{{ _('HTTP') }}</th>
<th scope="col">{{ _('Processing') }}</th>
</tr>
<tr>
<th scope="col">{{ _('Median') }}</th>
<td>{{ engine_stat.total }}</td>
<td>{{ engine_stat.http }}</td>
<td>{{ engine_stat.processing }}</td>
</tr>
<tr>
<th scope="col">{{ _('P80') }}</th>
<td>{{ engine_stat.total_p80 }}</td>
<td>{{ engine_stat.http_p80 }}</td>
<td>{{ engine_stat.processing_p80 }}</td>
</tr>
<tr>
<th scope="col">{{ _('P95') }}</th>
<td>{{ engine_stat.total_p95 }}</td>
<td>{{ engine_stat.http_p95 }}</td>
<td>{{ engine_stat.processing_p95 }}</td>
</tr>
</table>
</div>
{%- endif -%}
</td>
<td style="text-align: right;"> {{ engine_reliabilities.get(engine_stat.name, {}).get('reliablity') }}</td>
</tr>
{% endfor %}
</table>
{% endif %}
{% endblock %}

View File

@ -93,7 +93,7 @@ from searx.preferences import Preferences, ValidationException, LANGUAGE_CODES
from searx.answerers import answerers
from searx.network import stream as http_stream
from searx.answerers import ask
from searx.metrics import get_engines_stats, get_engine_errors, histogram, counter
from searx.metrics import get_engines_stats, get_engine_errors, get_reliabilities, histogram, counter
# serve pages with HTTP/1.1
from werkzeug.serving import WSGIRequestHandler
@ -1073,16 +1073,47 @@ def image_proxy():
@app.route('/stats', methods=['GET'])
def stats():
"""Render engine statistics page."""
checker_results = checker_get_result()
checker_results = checker_results['engines'] \
if checker_results['status'] == 'ok' and 'engines' in checker_results else {}
filtered_engines = dict(filter(lambda kv: (kv[0], request.preferences.validate_token(kv[1])), engines.items()))
engine_stats = get_engines_stats(filtered_engines)
engine_reliabilities = get_reliabilities(filtered_engines, checker_results)
sort_order = request.args.get('sort', default='name', type=str)
SORT_PARAMETERS = {
'name': (False, 'name', ''),
'score': (True, 'score', 0),
'result_count': (True, 'result_count', 0),
'time': (False, 'total', 0),
'reliability': (False, 'reliability', 100),
}
if sort_order not in SORT_PARAMETERS:
sort_order = 'name'
reverse, key_name, default_value = SORT_PARAMETERS[sort_order]
def get_key(engine_stat):
reliability = engine_reliabilities.get(engine_stat['name']).get('reliablity', 0)
reliability_order = 0 if reliability else 1
if key_name == 'reliability':
key = reliability
reliability_order = 0
else:
key = engine_stat.get(key_name) or default_value
if reverse:
reliability_order = 1 - reliability_order
return (reliability_order, key, engine_stat['name'])
engine_stats['time'] = sorted(engine_stats['time'], reverse=reverse, key=get_key)
return render(
'stats.html',
stats=[(gettext('Engine time (sec)'), engine_stats['time_total']),
(gettext('Page loads (sec)'), engine_stats['time_http']),
(gettext('Number of results'), engine_stats['result_count']),
(gettext('Scores'), engine_stats['scores']),
(gettext('Scores per result'), engine_stats['scores_per_result']),
(gettext('Errors'), engine_stats['error_count'])]
sort_order=sort_order,
engine_stats=engine_stats,
engine_reliabilities=engine_reliabilities,
)