> */ public function probeRecentErrorSignatures(): array { $out = array(); $log = function_exists('abj_service_optional') ? abj_service_optional('logging') : null; if (!is_object($log) || !method_exists($log, 'getDebugFilePath')) { return $out; } $path = (string)$log->getDebugFilePath(); if ($path === '' || !is_file($path) || !is_readable($path)) { return $out; } $size = @filesize($path); if ($size === false || $size === 0) { return $out; } $readBytes = 262144; // 256 KB $offset = $size > $readBytes ? $size - $readBytes : 0; $fh = @fopen($path, 'rb'); if (!is_resource($fh)) { return $out; } $tail = ''; try { if ($offset > 0) { @fseek($fh, $offset); // Discard the partial first line so we only group on whole records. @fgets($fh); } $chunk = @fread($fh, $readBytes); if (is_string($chunk)) { $tail = $chunk; } } finally { @fclose($fh); } if ($tail === '') { return $out; } $cutoff = abj_clock()->now() - 7 * 86400; $byKey = array(); $lines = preg_split('/\r?\n/', $tail); if (!is_array($lines)) { return $out; } foreach ($lines as $line) { if (!is_string($line) || $line === '') { continue; } // Match "YYYY-MM-DD HH:MM:SS (LEVEL): tail..." per Logging.php format. if (!preg_match('/^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}) \((ERROR|WARN)\):\s*(.*)$/', $line, $m)) { continue; } $ts = strtotime($m[1]); if ($ts === false || $ts < $cutoff) { continue; } $level = $m[2]; $msg = trim($m[3]); if ($msg === '') { continue; } $sig = $level . ':' . substr($this->normalizeErrorSignature($msg), 0, 200); if (!isset($byKey[$sig])) { $byKey[$sig] = array('signature' => $sig, 'count' => 0, 'last_seen_at' => 0); } $byKey[$sig]['count']++; if ($ts > $byKey[$sig]['last_seen_at']) { $byKey[$sig]['last_seen_at'] = $ts; } } if (empty($byKey)) { return $out; } $list = array_values($byKey); usort($list, function ($a, $b) { $cmp = $b['count'] - $a['count']; if ($cmp !== 0) { return $cmp; } return $b['last_seen_at'] - $a['last_seen_at']; }); return array_slice($list, 0, 5); } /** * Coarse-grain an error message so different incident timestamps, * memory addresses, file paths, and line numbers fold into the same * signature. Used by probeRecentErrorSignatures to group recurring * errors. Exposed (public) so the unit test * tests/F6UrlFragmentLeakIntoErrorSignatureTest can pin the * PII-stripping behavior directly. * * @param string $msg * @return string */ public function normalizeErrorSignature(string $msg): string { $s = $msg; // Strip absolute paths to just the basename. $s = preg_replace('#/[A-Za-z0-9_\-\./]+/([A-Za-z0-9_\-]+\.php)#', '$1', $s) ?? $s; // Collapse memory addresses, hex, and digit sequences. $s = preg_replace('/\b0x[0-9a-fA-F]+\b/', '0xN', $s) ?? $s; $s = preg_replace('/\b\d{4,}\b/', 'N', $s) ?? $s; // Collapse runs of whitespace. $s = preg_replace('/\s+/', ' ', $s) ?? $s; return trim($s); } }