Add HTTP performance log analyzer (M12.6).

Parse rotated nginx JSON access logs into host/route timings so production hotspots can be measured without ad-hoc one-off scripts.
This commit is contained in:
2026-08-29 12:25:27 +02:00
parent 4ab315e9d7
commit fb560fb7dd
10 changed files with 737 additions and 139 deletions
+68 -18
View File
@@ -3,10 +3,18 @@
declare(strict_types=1);
/**
* Read-only analyzer for /var/log/nginx/skinbase-performance.log
* Read-only analyzer for nginx skinbase-performance JSON logs.
*
* Usage:
* php scripts/analyze-http-performance.php --file=/var/log/nginx/skinbase-performance.log --since=24h --top=20 --host=skinbase.org
* Single file (backward compatible):
* php scripts/analyze-http-performance.php --file=/var/log/nginx/skinbase-performance.log --since=24h --host=skinbase.org
*
* Rotation-aware 24h:
* php scripts/analyze-http-performance.php --glob='/var/log/nginx/skinbase-performance.log*' --since=24h --host=skinbase.org
*
* Status:
* --status=404 exact HTTP 404
* --status=4xx 400-499
* --status=5xx 500-599
*/
use App\Support\Http\HttpPerformanceLogAnalyzer;
@@ -14,9 +22,11 @@ use App\Support\Http\HttpUriNormalizer;
require __DIR__.'/../vendor/autoload.php';
$files = [];
$globs = [];
$options = [
'file' => '/var/log/nginx/skinbase-performance.log',
'since' => '24h',
'until' => null,
'min_requests' => 1,
'top' => 20,
'status' => null,
@@ -31,9 +41,18 @@ foreach (array_slice($argv, 1) as $arg) {
exit(2);
}
[$key, $value] = array_pad(explode('=', substr($arg, 2), 2), 2, '1');
if ($key === 'file') {
$files[] = $value;
continue;
}
if ($key === 'glob') {
$globs[] = $value;
continue;
}
$map = [
'file' => 'file',
'since' => 'since',
'until' => 'until',
'hours' => 'hours',
'min-requests' => 'min_requests',
'top' => 'top',
'status' => 'status',
@@ -48,21 +67,52 @@ foreach (array_slice($argv, 1) as $arg) {
$options[$map[$key]] = $value;
}
$file = (string) $options['file'];
if (! is_readable($file)) {
fwrite(STDERR, "Cannot read {$file}\n");
exit(1);
if (isset($options['hours']) && is_numeric($options['hours'])) {
$options['since'] = ((int) $options['hours']).'h';
}
$analyzer = new HttpPerformanceLogAnalyzer(new HttpUriNormalizer());
$result = $analyzer->analyze($file, [
'since' => $options['since'],
'min_requests' => (int) $options['min_requests'],
'top' => (int) $options['top'],
'status' => $options['status'],
'uri' => $options['uri'],
'host' => $options['host'],
'slow_ms' => $options['slow_ms'] !== null ? (float) $options['slow_ms'] : null,
]);
$discovered = $files;
foreach ($globs as $pattern) {
$discovered = array_merge($discovered, $analyzer->expandGlob($pattern));
}
if ($discovered === [] && $globs === []) {
$discovered[] = '/var/log/nginx/skinbase-performance.log';
}
$discovered = $analyzer->deduplicatePaths($discovered);
if ($discovered === []) {
fwrite(STDERR, "No matching performance log files.\n");
exit(1);
}
foreach ($discovered as $path) {
if (! is_readable($path)) {
fwrite(STDERR, "Cannot read {$path}\n");
exit(1);
}
}
try {
$result = $analyzer->analyze($discovered, [
'since' => $options['since'],
'until' => $options['until'],
'min_requests' => (int) $options['min_requests'],
'top' => (int) $options['top'],
'status' => $options['status'],
'uri' => $options['uri'],
'host' => $options['host'],
'slow_ms' => $options['slow_ms'] !== null ? (float) $options['slow_ms'] : null,
]);
} catch (InvalidArgumentException $e) {
fwrite(STDERR, $e->getMessage()."\n");
exit(2);
}
foreach ($result['warnings'] ?? [] as $warning) {
fwrite(STDERR, $warning."\n");
}
echo json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES), PHP_EOL;