errorLogger = $errorLogger;
}
/**
* Return the latest reportable ERROR line and total reportable error count.
*
* @param string $debugPath
* @return array{num: int, line: string|null, total_error_count: int}
*/
public function getLatestErrorLine(string $debugPath): array {
$f = abj_service('functions');
$latestErrorLineFound = array(
'num' => -1,
'line' => null,
'total_error_count' => 0,
);
$linesRead = 0;
$handle = null;
$collectingErrorLines = false;
try {
if ($debugPath === '' || !file_exists($debugPath)) {
return $latestErrorLineFound;
}
if ($handle = fopen($debugPath, "r")) {
while (($line = fgets($handle)) !== false) {
$linesRead++;
$hasError = stripos($line, '(ERROR)');
$isDeleteError = stripos($line, 'SQL query error: DELETE command denied to user');
if ($hasError !== false && $isDeleteError === false) {
$latestErrorLineFound['num'] = $linesRead;
$latestErrorLineFound['line'] = $line;
$latestErrorLineFound['total_error_count'] += 1;
$collectingErrorLines = true;
} else if ($collectingErrorLines &&
!$f->regexMatch("^\d{4}[-]\d{2}[-]\d{2} .*\(\w+\):\s.*$", $line)) {
$latestErrorLineFound['line'] .= "
\n" . $line;
} else {
$collectingErrorLines = false;
}
}
} else {
call_user_func($this->errorLogger, "Error reading log file (1).");
}
} catch (Exception $e) {
call_user_func($this->errorLogger, "Error reading log file. (2)", $e);
}
if ($handle != null) {
fclose($handle);
}
return $latestErrorLineFound;
}
/**
* Build a sanitized support excerpt from recent ERROR/WARN entries and
* recent context lines.
*
* @param string $debugFilePath
* @return string
*/
public function getSanitizedLogExcerptForSupport(string $debugFilePath): string {
try {
if (!file_exists($debugFilePath)) {
return "No log file available";
}
$excerptParts = $this->readSupportExcerptParts($debugFilePath);
if (!$excerptParts['readable']) {
return "Log file not readable";
}
return $this->formatSupportExcerpt($excerptParts['error_entries'], $excerptParts['recent_lines']);
} catch (Exception $e) {
abj404_logPhpFallback('logger-internal', 'support log excerpt read failed: ' . $e->getMessage());
return "Error reading log file: " . $e->getMessage();
}
}
/**
* @param string $debugFilePath
* @return array{readable: bool, error_entries: array>, recent_lines: array}
*/
private function readSupportExcerptParts(string $debugFilePath): array {
$errorEntries = array();
$recentLines = array();
$currentEntry = array();
$collectingEntry = false;
$handle = fopen($debugFilePath, "r");
if (!$handle) {
return array('readable' => false, 'error_entries' => array(), 'recent_lines' => array());
}
while (($line = fgets($handle)) !== false) {
$this->collectSupportExcerptLine(
(string)$line,
$errorEntries,
$recentLines,
$currentEntry,
$collectingEntry
);
}
$this->storeCurrentSupportEntry($errorEntries, $currentEntry);
fclose($handle);
return array(
'readable' => true,
'error_entries' => $errorEntries,
'recent_lines' => $recentLines,
);
}
/**
* @param array> $errorEntries
* @param array $recentLines
* @param array $currentEntry
* @param bool $collectingEntry
* @return void
*/
private function collectSupportExcerptLine(
string $line,
array &$errorEntries,
array &$recentLines,
array &$currentEntry,
bool &$collectingEntry
): void {
$this->rememberRecentLine($recentLines, $line);
if ($this->isReportableWarningOrError($line)) {
if ($collectingEntry) {
$this->storeCurrentSupportEntry($errorEntries, $currentEntry);
}
$currentEntry = array($line);
$collectingEntry = true;
return;
}
if ($collectingEntry && $this->isContinuationLine($line)) {
$currentEntry[] = $line;
return;
}
if ($collectingEntry) {
$this->storeCurrentSupportEntry($errorEntries, $currentEntry);
}
$collectingEntry = false;
$currentEntry = array();
}
/**
* @param array $recentLines
* @return void
*/
private function rememberRecentLine(array &$recentLines, string $line): void {
$recentLines[] = $line;
if (count($recentLines) > 20) {
array_shift($recentLines);
}
}
private function isReportableWarningOrError(string $line): bool {
$hasError = stripos($line, '(ERROR)') !== false;
$hasWarn = stripos($line, '(WARN)') !== false;
$isDeleteError = stripos($line, 'SQL query error: DELETE command denied to user') !== false;
return ($hasError || $hasWarn) && !$isDeleteError;
}
private function isContinuationLine(string $line): bool {
$f = abj_service('functions');
return !$f->regexMatch("^\d{4}[-]\d{2}[-]\d{2} .*\(\w+\):\s.*$", $line);
}
/**
* @param array> $errorEntries
* @param array $currentEntry
* @return void
*/
private function storeCurrentSupportEntry(array &$errorEntries, array &$currentEntry): void {
if (empty($currentEntry)) {
return;
}
$errorEntries[] = $currentEntry;
if (count($errorEntries) > 15) {
array_shift($errorEntries);
}
$currentEntry = array();
}
/**
* @param array> $errorEntries
* @param array $recentLines
* @return string
*/
private function formatSupportExcerpt(array $errorEntries, array $recentLines): string {
if (empty($errorEntries)) {
if (empty($recentLines)) {
return "Log file is empty";
}
$output = "No ERROR/WARN entries found. Last " . count($recentLines) . " log lines:\n\n";
$output .= implode("", $recentLines);
return trim($output);
}
$output = "Last " . count($errorEntries) . " ERROR/WARN entries:\n\n";
foreach ($errorEntries as $entry) {
$output .= implode("\n", $entry) . "\n\n";
}
if (!empty($recentLines)) {
$output .= "Recent context (last " . count($recentLines) . " lines):\n\n";
$output .= implode("", $recentLines);
}
return trim($output);
}
}