diff --git a/INSTALL b/INSTALL index 9bcd0759..b161edd2 100644 --- a/INSTALL +++ b/INSTALL @@ -49,9 +49,15 @@ read the instructions below. # pacman -S python-mysql-connector python-pygit2 python-srcinfo python-sqlalchemy \ python-bleach python-markdown python-alembic python-jinja \ - python-itsdangerous python-authlib python-httpx hypercorn + python-itsdangerous python-authlib python-httpx hypercorn \ + composer # python3 setup.py install +4a) Install `composer` dependencies while inside of aurweb's root: + + $ cd /path/to/aurweb + /path/to/aurweb $ composer require promphp/prometheus_client_php + 5) Create a new MySQL database and a user and import the aurweb SQL schema: $ python -m aurweb.initdb diff --git a/web/html/index.php b/web/html/index.php index e57e7708..82a44c55 100644 --- a/web/html/index.php +++ b/web/html/index.php @@ -3,10 +3,39 @@ set_include_path(get_include_path() . PATH_SEPARATOR . '../lib'); include_once("aur.inc.php"); include_once("pkgfuncs.inc.php"); +include_once("cachefuncs.inc.php"); +include_once("metricfuncs.inc.php"); $path = $_SERVER['PATH_INFO']; $tokens = explode('/', $path); +$query_string = $_SERVER['QUERY_STRING']; + +// If no options.cache is configured, we no-op metric storage operations. +$is_cached = defined('EXTENSION_LOADED_APC') || defined('EXTENSION_LOADED_MEMCACHE'); +if ($is_cached) { + $method = $_SERVER['REQUEST_METHOD']; + // We'll always add +1 to our total request count to this $path, + // unless this path == /metrics. + if ($path !== "/metrics") + add_metric("http_requests_count", $method, $path); + + // Extract $type out of $query_string, if we can. + $type = null; + $query = array(); + if ($query_string) + parse_str($query_string, $query); + $type = $query['type']; + + // Only store RPC metrics for valid types. + $good_types = [ + "info", "multiinfo", "search", "msearch", + "suggest", "suggest-pkgbase", "get-comment-form" + ]; + if ($path === "/rpc" && in_array($type, $good_types)) + add_metric("api_requests_count", $method, $path, $type); +} + if (config_get_bool('options', 'enable-maintenance') && (empty($tokens[1]) || ($tokens[1] != "css" && $tokens[1] != "images"))) { if (!in_array($_SERVER['REMOTE_ADDR'], explode(" ", config_get('options', 'maintenance-exceptions')))) { header("HTTP/1.0 503 Service Unavailable"); diff --git a/web/html/metrics.php b/web/html/metrics.php new file mode 100644 index 00000000..dfa860ed --- /dev/null +++ b/web/html/metrics.php @@ -0,0 +1,16 @@ + diff --git a/web/lib/metricfuncs.inc.php b/web/lib/metricfuncs.inc.php new file mode 100644 index 00000000..acfc30d7 --- /dev/null +++ b/web/lib/metricfuncs.inc.php @@ -0,0 +1,129 @@ +, 'query_string': }. + $metrics = get_cache_value("prometheus_metrics"); + $metrics = $metrics ? json_decode($metrics) : array(); + + $key = "$path:$type"; + + // If the current request $path isn't yet in $metrics create + // a new assoc array for it and push it into $metrics. + if (!in_array($key, $metrics)) { + $data = array( + 'anchor' => $anchor, + 'method' => $method, + 'path' => $path, + 'type' => $type + ); + array_push($metrics, json_encode($data)); + } + + // Cache-wise, we also store the count values of each route + // through the "prometheus:" key. Grab the cache value + // representing the current $path we're on (defaulted to 1). + $count = get_cache_value("prometheus:$key"); + $count = $count ? $count + 1 : 1; + + $labels = ["method", "route"]; + if ($type) + array_push($labels, "type"); + + $gauge = $registry->getOrRegisterGauge( + 'aurweb', + $anchor, + 'A metric count for the aurweb platform.', + $labels + ); + + $label_values = [$data['method'], $data['path']]; + if ($type) + array_push($label_values, $type); + + $gauge->set($count, $label_values); + + // Update cache values. + set_cache_value("prometheus:$key", $count, 0); + set_cache_value("prometheus_metrics", json_encode($metrics), 0); + +} + +function render_metrics() { + if (!defined('EXTENSION_LOADED_APC') && !defined('EXTENSION_LOADED_MEMCACHE')) { + error_log("The /metrics route requires a valid 'options.cache' " + . "configuration; no cache is configured."); + return http_response_code(417); // EXPECTATION_FAILED + } + + global $registry; + + // First, we grab the set of metrics we're interested in in the + // form of a cached JSON list, if we can. + $metrics = get_cache_value("prometheus_metrics"); + if (!$metrics) + $metrics = array(); + else + $metrics = json_decode($metrics); + + // Now, we walk through each of those list values one by one, + // which happen to be JSON-serialized associative arrays, + // and process each metric via its associative array's contents: + // The route path and the query string. + // See web/html/index.php for the creation of such metrics. + foreach ($metrics as $metric) { + $data = json_decode($metric, true); + + $anchor = $data['anchor']; + $path = $data['path']; + $type = $data['type']; + $key = "$path:$type"; + + $labels = ["method", "route"]; + if ($type) + array_push($labels, "type"); + + $count = get_cache_value("prometheus:$key"); + $gauge = $registry->getOrRegisterGauge( + 'aurweb', + $anchor, + 'A metric count for the aurweb platform.', + $labels + ); + + $label_values = [$data['method'], $data['path']]; + if ($type) + array_push($label_values, $type); + + $gauge->set($count, $label_values); + } + + // Construct the results from RenderTextFormat renderer and + // registry's samples. + $renderer = new RenderTextFormat(); + $result = $renderer->render($registry->getMetricFamilySamples()); + + // Output the results with the right content type header. + http_response_code(200); // OK + header('Content-Type: ' . RenderTextFormat::MIME_TYPE); + echo $result; +} + +?> diff --git a/web/lib/routing.inc.php b/web/lib/routing.inc.php index 73c667d2..0f452f22 100644 --- a/web/lib/routing.inc.php +++ b/web/lib/routing.inc.php @@ -19,7 +19,8 @@ $ROUTES = array( '/rss' => 'rss.php', '/tos' => 'tos.php', '/tu' => 'tu.php', - '/addvote' => 'addvote.php', + '/addvote' => 'addvote.php', + '/metrics' => 'metrics.php' // Prometheus Metrics ); $PKG_PATH = '/packages';