diff options
Diffstat (limited to 'modules-available/statistics/pages/summary.inc.php')
-rw-r--r-- | modules-available/statistics/pages/summary.inc.php | 364 |
1 files changed, 364 insertions, 0 deletions
diff --git a/modules-available/statistics/pages/summary.inc.php b/modules-available/statistics/pages/summary.inc.php new file mode 100644 index 00000000..905f5d90 --- /dev/null +++ b/modules-available/statistics/pages/summary.inc.php @@ -0,0 +1,364 @@ +<?php + +class SubPage +{ + + private static $STATS_COLORS; + + public static function doPreprocess() + { + User::assertPermission('view.summary'); + if (!Module::isAvailable('js_chart')) { + ErrorHandler::traceError('js_chart not available'); + } + } + + public static function doRender() + { + $filters = StatisticsFilter::parseQuery(); + $filterSet = new StatisticsFilterSet($filters); + + if (!$filterSet->setAllowedLocationsFromPermission('view.summary')) { + Message::addError('main.no-permission'); + Util::redirect('?do=main'); + } + + // Prepare chart colors + self::$STATS_COLORS = []; + for ($i = 0; $i < 10; ++$i) { + self::$STATS_COLORS[] = '#55' . sprintf('%02s%02s', dechex( + (int)((($i + 1) * ($i + 1)) / .3922)), + dechex((int)(abs((5 - $i) * 51)))); + } + + $filterSet->filterNonClients(); + StatisticsFilter::renderFilterBox('summary', $filterSet); + Render::openTag('div', array('class' => 'row')); + self::showSummary($filterSet); + self::showMemory($filterSet); + self::showId44($filterSet); + self::showKvmState($filterSet); + self::showLatestMachines($filterSet); + self::showSystemModels($filterSet); + Render::closeTag('div'); + } + + private static function showSummary(StatisticsFilterSet $filterSet): void + { + $filterSet->makeFragments($where, $join, $args); + $known = Database::queryFirst("SELECT Count(*) AS val FROM machine m $join WHERE $where", $args); + $on = Database::queryFirst("SELECT Count(*) AS val FROM machine m $join WHERE state IN ('IDLE', 'OCCUPIED') AND ($where)", $args); + $used = Database::queryFirst("SELECT Count(*) AS val FROM machine m $join WHERE state = 'OCCUPIED' AND ($where)", $args); + $hdd = Database::queryFirst("SELECT Count(*) AS val FROM machine m $join WHERE badsectors >= 10 AND ($where)", $args); + if ($on['val'] != 0) { + $usedpercent = round($used['val'] / $on['val'] * 100); + } else { + $usedpercent = 0; + } + $data = [ + 'known' => $known['val'], + 'online' => $on['val'], + 'used' => $used['val'], + 'usedpercent' => $usedpercent, + 'badhdd' => $hdd['val'], + ]; + // Graph + $labels = []; + $points1 = []; + $points2 = []; + $lectures = []; + // Get locations + if ($filterSet->suitableForUsageGraph()) { + $locFilter = $filterSet->hasFilter('LocationStatisticsFilter'); + if ($locFilter === null + || ($locFilter->op === '~' && ($locFilter->argument == 0 + || (is_array($locFilter->argument) && in_array(0, $locFilter->argument))))) { + $locations = null; + $op = null; + } elseif ($locFilter->op === '~') { + $locations = array_keys(Location::getRecursiveFlat($locFilter->argument)); + $op = $locFilter->op; + } else { + if (is_array($locFilter->argument)) { + $locations = $locFilter->argument; + } else { + $locations = [$locFilter->argument]; + } + $op = $locFilter->op; + } + //error_log($op . ' ' . print_r($locations, true)); + $cutoff = time() - 2 * 86400; + $res = Database::simpleQuery("SELECT dateline, data FROM statistic + WHERE typeid = '~stats' AND dateline > $cutoff ORDER BY dateline DESC"); + // Get max from 4 consecutive values, which should be 4*5 = 20m + $sum = 0; + foreach ($res as $row) { + if ($row['data'][0] === '{') { + $x = json_decode($row['data'], true); + if (!is_array($x) || !isset($x['usage'])) + continue; + $x = self::mangleStatsJson($x, $locations, $op); + } else if ($locations === null) { + $x = explode('#', $row['data']); + if (count($x) < 3) + continue; + $x[] = 0; + } else { + continue; + } + if ($sum % 4 === 0) { + $labels[] = date('H:i', $row['dateline']); + } else { + $x[1] = max($x[1], array_pop($points1)); + $x[2] = max($x[2], array_pop($points2)); + $x[3] += array_pop($lectures); + } + $points1[] = $x[1]; + $points2[] = $x[2]; + $lectures[] = $x[3]; + ++$sum; + } + } + if (!empty($points1) && max($points1) > 0) { + $labels = array_reverse($labels); + $points1 = array_reverse($points1); + $points2 = array_reverse($points2); + $lectures = array_reverse($lectures); + $data['json'] = json_encode(['labels' => $labels, + 'datasets' => [ + ['data' => $points1, 'label' => 'Online', 'borderColor' => '#8f3'], + ['data' => $points2, 'label' => 'In use', 'borderColor' => '#e76'], + ]]); + $data['markings'] = json_encode($lectures); + } + if (Module::get('runmode') !== false) { + $res = Database::queryFirst('SELECT Count(*) AS cnt FROM machine m INNER JOIN runmode r USING (machineuuid)' + . " $join WHERE $where", $args); + $data['runmode'] = $res['cnt']; + } + // Draw + Render::addTemplate('summary', $data); + } + + private static function showSystemModels(StatisticsFilterSet $filterSet): void + { + $filterSet->makeFragments($where, $join, $args); + $res = Database::simpleQuery('SELECT systemmodel, Round(AVG(realcores)) AS cores, Count(*) AS `count` FROM machine m' + . " $join WHERE $where GROUP BY systemmodel ORDER BY `count` DESC, systemmodel ASC", $args); + $lines = []; + $json = []; + $id = 0; + foreach ($res as $row) { + if (empty($row['systemmodel'])) { + continue; + } + settype($row['count'], 'integer'); + $row['urlsystemmodel'] = urlencode($row['systemmodel']); + $row['idx'] = count($lines); + $lines[] = $row; + $json[] = [ + 'color' => self::$STATS_COLORS[$id % count(self::$STATS_COLORS)], + 'value' => $row['count'], + ]; + ++$id; + } + self::capChart($json, $lines, 0.92); + Render::addTemplate('cpumodels', ['rows' => $lines, 'json' => json_encode($json)]); + } + + private static function alignBySteps(int $value, array $steps): int + { + for ($i = 1; $i < count($steps); ++$i) { + if ($steps[$i] < $value) { + continue; + } + if ($steps[$i] - $value >= $value - $steps[$i - 1]) { + --$i; + } + return $steps[$i]; + } + return $value; + } + + private static function showMemory(StatisticsFilterSet $filterSet): void + { + $filterSet->makeFragments($where, $join, $args); + $res = Database::simpleQuery("SELECT mbram, Count(*) AS `count` FROM machine m $join + WHERE $where GROUP BY mbram", $args); + $lines = []; + foreach ($res as $row) { + $gb = self::alignBySteps((int)ceil($row['mbram'] / 1024), StatisticsFilter::SIZE_RAM); + $lines[$gb] = ($lines[$gb] ?? 0) + $row['count']; + } + asort($lines); + $data = ['rows' => []]; + $json = []; + $id = 0; + foreach (array_reverse($lines, true) as $k => $v) { + $data['rows'][] = [ + 'idx' => count($data['rows']), + 'gb' => $k, + 'count' => $v, + 'class' => StatisticsStyling::ramColorClass($k * 1024), + ]; + $json[] = [ + 'color' => self::$STATS_COLORS[$id % count(self::$STATS_COLORS)], + 'value' => $v, + ]; + ++$id; + } + self::capChart($json, $data['rows'], 0.92); + $data['json'] = json_encode($json); + Render::addTemplate('memory', $data); + } + + private static function showKvmState(StatisticsFilterSet $filterSet): void + { + $filterSet->makeFragments($where, $join, $args); + $colors = ['UNKNOWN' => '#666', 'UNSUPPORTED' => '#ea5', 'DISABLED' => '#e55', 'ENABLED' => '#6d6']; + $res = Database::simpleQuery("SELECT kvmstate, Count(*) AS `count` FROM machine m $join + WHERE $where GROUP BY kvmstate ORDER BY `count` DESC", $args); + $lines = []; + $json = []; + foreach ($res as $row) { + $row['idx'] = count($lines); + $lines[] = $row; + $json[] = array( + 'color' => $colors[$row['kvmstate']] ?? '#000', + 'value' => $row['count'], + ); + } + Render::addTemplate('kvmstate', array('rows' => $lines, 'json' => json_encode($json))); + } + + private static function showId44(StatisticsFilterSet $filterSet): void + { + $filterSet->makeFragments($where, $join, $args); + $res = Database::simpleQuery("SELECT id44mb, Count(*) AS `count` FROM machine m $join WHERE $where GROUP BY id44mb", $args); + $lines = array(); + $total = 0; + foreach ($res as $row) { + $total += $row['count']; + $gb = self::alignBySteps((int)ceil($row['id44mb'] / 1024), StatisticsFilter::SIZE_PARTITION); + $lines[$gb] = ($lines[$gb] ?? 0) + $row['count']; + } + asort($lines); + $data = array('rows' => array()); + $json = array(); + $id = 0; + foreach (array_reverse($lines, true) as $k => $v) { + $data['rows'][] = [ + 'idx' => count($data['rows']), + 'gb' => $k, + 'count' => $v, + 'class' => StatisticsStyling::hddColorClass($k), + ]; + if ($k === 0) { + $color = '#e55'; + } else { + $color = self::$STATS_COLORS[$id++ % count(self::$STATS_COLORS)]; + } + $json[] = array( + 'color' => $color, + 'value' => $v, + ); + } + self::capChart($json, $data['rows'], 0.95); + $data['json'] = json_encode($json); + Render::addTemplate('id44', $data); + } + + private static function showLatestMachines(StatisticsFilterSet $filterSet): void + { + $filterSet->makeFragments($where, $join, $args); + $args['cutoff'] = ceil(time() / 3600) * 3600 - 86400 * 10; + + $res = Database::simpleQuery("SELECT m.machineuuid, m.clientip, m.hostname, m.firstseen, m.mbram, m.kvmstate, m.id44mb + FROM machine m $join + WHERE firstseen > :cutoff AND $where + ORDER BY firstseen DESC LIMIT 32", $args); + $rows = array(); + $count = 0; + foreach ($res as $row) { + if (empty($row['hostname'])) { + $row['hostname'] = $row['clientip']; + } + $row['firstseen_int'] = $row['firstseen']; + $row['firstseen'] = Util::prettyTime($row['firstseen']); + $row['gbram'] = round(round($row['mbram'] / 500) / 2, 1); // Trial and error until we got "expected" rounding.. + $row['gbtmp'] = round($row['id44mb'] / 1024); + $row['ramclass'] = StatisticsStyling::ramColorClass((int)$row['mbram']); + $row['kvmclass'] = StatisticsStyling::kvmColorClass($row['kvmstate']); + $row['hddclass'] = StatisticsStyling::hddColorClass((int)$row['gbtmp']); + $row['kvmicon'] = $row['kvmstate'] === 'ENABLED' ? '✓' : '✗'; + if (++$count > 5) { + $row['collapse'] = 'collapse'; + } + $rows[] = $row; + } + Render::addTemplate('newclients', array('rows' => $rows, 'openbutton' => $count > 5)); + } + + /* + * HELPERS + */ + + + + private static function capChart(array &$json, array &$rows, float $cutoff, float $minSlice = 0.015): void + { + $total = 0; + foreach ($json as $entry) { + $total += $entry['value']; + } + if ($total === 0) { + return; + } + $cap = ceil($total * $cutoff); + $accounted = 0; + $id = 0; + foreach ($json as $entry) { + if (($accounted >= $cap || $entry['value'] / $total < $minSlice) && $id >= 3) { + break; + } + ++$id; + $accounted += $entry['value']; + } + for ($i = $id; $i < count($rows); ++$i) { + $rows[$i]['collapse'] = 'collapse'; + } + $json = array_slice($json, 0, $id); + if ($accounted / $total < 0.99) { + $json[] = array( + 'color' => '#eee', + 'label' => 'invalid', + 'value' => ($total - $accounted), + ); + } + } + + /** + * @param array $json decoded json ~stats data + * @param ?int[] $locations + */ + private static function mangleStatsJson(array $json, ?array $locations, ?string $op): array + { + // Total, On, InUse, Lectures + $retval = [0, 0, 0, 0]; + foreach ($json['usage'] as $lid => $data) { + $lid = (int)$lid; + if ($locations === null + || ($op === '!=' && !in_array($lid, $locations)) + || ($op !== '!=' && in_array($lid, $locations))) { + $retval[0] += $data['t']; + $retval[1] += $data['o'] ?? 0; + $retval[2] += $data['u'] ?? 0; + if (isset($data['event'])) { + $retval[3] += 1; + } + } + } + return $retval; + } + +} |