The big response time number is labelled last check. ClientErrorClassifier tells smoke and curl test reports and bot reports apart; tests are counted separately and left out of the tile, the hourly bars, the list and the dashboard tile, bots keep a label. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
132 lines
5.9 KiB
PHP
132 lines
5.9 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
namespace Evomedien\Vitec\Widgets;
|
||
|
||
use Evomedien\Vitec\Service\ClientErrorClassifier;
|
||
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||
use TYPO3\CMS\Dashboard\Widgets\AdditionalCssInterface;
|
||
use TYPO3\CMS\Dashboard\Widgets\WidgetConfigurationInterface;
|
||
use TYPO3\CMS\Dashboard\Widgets\WidgetInterface;
|
||
|
||
/**
|
||
* Dashboard tile: purge and health statistics from the two protocol
|
||
* tables. History depth = what vitec:ssr-health sampling (cron, every
|
||
* 5 minutes) and the purge log hold (14 days). Cockpit look (CockpitHtml):
|
||
* availability strip of the last 24 h plus number tiles.
|
||
*/
|
||
final class SsrStatsWidget implements WidgetInterface, AdditionalCssInterface
|
||
{
|
||
public function __construct(
|
||
private readonly WidgetConfigurationInterface $configuration,
|
||
private readonly ConnectionPool $connectionPool,
|
||
) {}
|
||
|
||
public function renderWidgetContent(): string
|
||
{
|
||
$h = CockpitHtml::class;
|
||
$now = time();
|
||
$d1 = $now - 86400;
|
||
$d7 = $now - 7 * 86400;
|
||
|
||
$purges24h = $this->count('tx_vitec_ssrpurgelog', 'crdate > ' . $d1);
|
||
$purges7d = $this->count('tx_vitec_ssrpurgelog', 'crdate > ' . $d7);
|
||
$failed7d = $this->count('tx_vitec_ssrpurgelog', 'crdate > ' . $d7 . ' AND success = 0');
|
||
$full7d = $this->count('tx_vitec_ssrpurgelog', "crdate > " . $d7 . " AND paths = '*'");
|
||
$avgMs = $this->scalar('SELECT ROUND(AVG(response_ms)) FROM tx_vitec_ssrhealthlog WHERE crdate > ' . $d1 . ' AND ok = 1');
|
||
$maxCache = $this->scalar('SELECT MAX(cache_mb) FROM tx_vitec_ssrhealthlog WHERE crdate > ' . $d7);
|
||
// Browser-side errors reported via POST /api/vitec/client-error,
|
||
// without smoke/curl test reports (0114-FE).
|
||
[$clientErrors24h, $clientErrors7d] = $this->realClientErrors($d1, $d7);
|
||
|
||
$strip = $h::strip($this->connectionPool);
|
||
$uptime = $strip['uptime'];
|
||
$uptimeTone = $uptime === null ? '' : ($uptime >= 99.5 ? 'vcw-ok' : ($uptime >= 95 ? 'vcw-warn' : 'vcw-bad'));
|
||
|
||
$html = '<div class="vcw-strip-h"><div><div class="vcw-label">Availability · 24 h</div>'
|
||
. '<div class="vcw-num ' . $uptimeTone . '">' . ($uptime === null ? '–' : number_format($uptime, 1)) . '<small>%</small></div></div>'
|
||
. '<div class="vcw-sub vcw-right">' . $strip['samples'] . ' samples<br>' . $strip['slow'] . ' slow · '
|
||
. $strip['down'] . ' down</div></div>'
|
||
. $strip['html'];
|
||
|
||
$tile = static fn(string $label, string $value, string $sub = '', string $tone = ''): string =>
|
||
'<div class="vcw-tile"><div class="vcw-label">' . $h::e($label) . '</div><div class="vcw-val ' . $tone . '">'
|
||
. $value . '</div>' . ($sub !== '' ? '<div class="vcw-sub">' . $sub . '</div>' : '') . '</div>';
|
||
|
||
$html .= '<div class="vcw-tiles">'
|
||
. $tile('Purges', $purges24h . '<small>/ ' . $purges7d . '</small>', '24 h / 7 days')
|
||
. $tile('Full purges', (string)$full7d, 'last 7 days', $full7d > 0 ? 'vcw-warn' : '')
|
||
. $tile('Failed purges', (string)$failed7d, 'last 7 days', $failed7d > 0 ? 'vcw-bad' : 'vcw-ok')
|
||
. $tile('Avg. response', $avgMs !== null ? $avgMs . '<small>ms</small>' : '–', 'last 24 h')
|
||
. $tile('Max. cache', $maxCache !== null ? $maxCache . '<small>MB</small>' : '–', 'last 7 days')
|
||
. $tile('Client errors', $clientErrors24h . '<small>/ ' . $clientErrors7d . '</small>', '24 h / 7 days, no tests', $clientErrors24h > 0 ? 'vcw-warn' : 'vcw-ok')
|
||
. '</div>';
|
||
|
||
if ($strip['samples'] === 0) {
|
||
$html .= '<p class="vcw-dim">No samples yet: the cron job <code>vitec:ssr-health</code> fills this every 5 minutes.</p>';
|
||
}
|
||
$html .= '<div class="vcw-foot">' . $h::moduleLink('Charts and protocol in the cockpit') . '</div>';
|
||
return $h::wrap($html, 'Availability of the SSR node over the last 24 hours, one bar per 15 minutes: '
|
||
. 'green ok, yellow slower than 400 ms, red down, grey no sample. Below are counts from the purge and '
|
||
. 'health logs: purges, full purges and failed purges, average response time, the largest cache size '
|
||
. 'and the browser errors reported by the frontend.', 'stats');
|
||
}
|
||
|
||
public function getOptions(): array
|
||
{
|
||
return [];
|
||
}
|
||
|
||
public function getCssFiles(): array
|
||
{
|
||
return [CockpitHtml::CSS];
|
||
}
|
||
|
||
/**
|
||
* @return array{0: int, 1: int} real client error reports in 24 h / 7 days
|
||
*/
|
||
private function realClientErrors(int $d1, int $d7): array
|
||
{
|
||
try {
|
||
$rows = $this->connectionPool->getConnectionForTable('tx_vitec_clienterrorlog')->executeQuery(
|
||
'SELECT crdate, message, user_agent, `release` FROM tx_vitec_clienterrorlog WHERE crdate > ?',
|
||
[$d7]
|
||
)->fetchAllAssociative();
|
||
} catch (\Throwable) {
|
||
return [0, 0];
|
||
}
|
||
$day = 0;
|
||
$week = 0;
|
||
foreach ($rows as $r) {
|
||
if (ClientErrorClassifier::isTest((string)$r['message'], (string)$r['user_agent'], (string)$r['release'])) {
|
||
continue;
|
||
}
|
||
$week++;
|
||
$day += (int)$r['crdate'] > $d1 ? 1 : 0;
|
||
}
|
||
return [$day, $week];
|
||
}
|
||
|
||
private function count(string $table, string $where): int
|
||
{
|
||
try {
|
||
return (int)$this->connectionPool->getConnectionForTable($table)
|
||
->executeQuery('SELECT COUNT(*) FROM ' . $table . ' WHERE ' . $where)->fetchOne();
|
||
} catch (\Throwable) {
|
||
return 0;
|
||
}
|
||
}
|
||
|
||
private function scalar(string $sql): ?string
|
||
{
|
||
try {
|
||
$v = $this->connectionPool->getConnectionForTable('tx_vitec_ssrhealthlog')
|
||
->executeQuery($sql)->fetchOne();
|
||
return $v === null || $v === false ? null : (string)$v;
|
||
} catch (\Throwable) {
|
||
return null;
|
||
}
|
||
}
|
||
}
|