Files
orascheandClaude Opus 5.5 06b9bc8293 SSR cockpit: label the response time, leave test reports out of the client error counts (0114-FE)
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>
2026-11-12 11:42:57 +01:00

132 lines
5.9 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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;
}
}
}