PluginProbe
404 Solution / 4.3.0
404 Solution v4.3.0
4.3.5 4.3.4 4.3.3 4.3.2 4.3.1 4.3.0 4.2.0 4.1.19 4.1.18 4.1.17 4.1.16 4.1.15 4.1.13 4.1.12 4.1.11 4.1.10 4.1.9 4.1.8 4.1.7 4.1.6 4.1.5 4.1.4 4.1.3 trunk 2.30.0 All 109 releases
404-solution / includes / diagnostics / DebugLogReader.php

DebugLogReader.php in 404 Solution 4.3.0, at includes/diagnostics/DebugLogReader.php

240 lines 8.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3
4 if (!defined('ABSPATH')) {
5 exit;
6 }
7
8 // allow-no-test-found: covered by tests/LoggingTest.php and tests/LogExcerptAdminActionsTest.php through public Logging entry points.
9
10 /**
11 * Reader/parser for debug-log files.
12 *
13 * Owns multiline ERROR/WARN parsing for cron error reports and support
14 * excerpts. It does not know where the log file lives; callers provide a
15 * concrete path from the debug-log store.
16 */
17 class ABJ_404_Solution_DebugLogReader {
18
19 /** @var callable */
20 private $errorLogger;
21
22 /** @param callable $errorLogger Receives message and optional exception. */
23 public function __construct(callable $errorLogger) {
24 $this->errorLogger = $errorLogger;
25 }
26
27 /**
28 * Return the latest reportable ERROR line and total reportable error count.
29 *
30 * @param string $debugPath
31 * @return array{num: int, line: string|null, total_error_count: int}
32 */
33 public function getLatestErrorLine(string $debugPath): array {
34 $f = abj_service('functions');
35 $latestErrorLineFound = array(
36 'num' => -1,
37 'line' => null,
38 'total_error_count' => 0,
39 );
40 $linesRead = 0;
41 $handle = null;
42 $collectingErrorLines = false;
43 try {
44 if ($debugPath === '' || !file_exists($debugPath)) {
45 return $latestErrorLineFound;
46 }
47 if ($handle = fopen($debugPath, "r")) {
48 while (($line = fgets($handle)) !== false) {
49 $linesRead++;
50 $hasError = stripos($line, '(ERROR)');
51 $isDeleteError = stripos($line, 'SQL query error: DELETE command denied to user');
52 if ($hasError !== false && $isDeleteError === false) {
53 $latestErrorLineFound['num'] = $linesRead;
54 $latestErrorLineFound['line'] = $line;
55 $latestErrorLineFound['total_error_count'] += 1;
56 $collectingErrorLines = true;
57 } else if ($collectingErrorLines &&
58 !$f->regexMatch("^\d{4}[-]\d{2}[-]\d{2} .*\(\w+\):\s.*$", $line)) {
59 $latestErrorLineFound['line'] .= "<BR/>\n" . $line;
60 } else {
61 $collectingErrorLines = false;
62 }
63 }
64 } else {
65 call_user_func($this->errorLogger, "Error reading log file (1).");
66 }
67 } catch (Exception $e) {
68 call_user_func($this->errorLogger, "Error reading log file. (2)", $e);
69 }
70
71 if ($handle != null) {
72 fclose($handle);
73 }
74
75 return $latestErrorLineFound;
76 }
77
78 /**
79 * Build a sanitized support excerpt from recent ERROR/WARN entries and
80 * recent context lines.
81 *
82 * @param string $debugFilePath
83 * @return string
84 */
85 public function getSanitizedLogExcerptForSupport(string $debugFilePath): string {
86 try {
87 if (!file_exists($debugFilePath)) {
88 return "No log file available";
89 }
90 $excerptParts = $this->readSupportExcerptParts($debugFilePath);
91 if (!$excerptParts['readable']) {
92 return "Log file not readable";
93 }
94 return $this->formatSupportExcerpt($excerptParts['error_entries'], $excerptParts['recent_lines']);
95 } catch (Exception $e) {
96 abj404_logPhpFallback('logger-internal', 'support log excerpt read failed: ' . $e->getMessage());
97 return "Error reading log file: " . $e->getMessage();
98 }
99 }
100
101 /**
102 * @param string $debugFilePath
103 * @return array{readable: bool, error_entries: array<int, array<int, string>>, recent_lines: array<int, string>}
104 */
105 private function readSupportExcerptParts(string $debugFilePath): array {
106 $errorEntries = array();
107 $recentLines = array();
108 $currentEntry = array();
109 $collectingEntry = false;
110 $handle = fopen($debugFilePath, "r");
111
112 if (!$handle) {
113 return array('readable' => false, 'error_entries' => array(), 'recent_lines' => array());
114 }
115
116 while (($line = fgets($handle)) !== false) {
117 $this->collectSupportExcerptLine(
118 (string)$line,
119 $errorEntries,
120 $recentLines,
121 $currentEntry,
122 $collectingEntry
123 );
124 }
125
126 $this->storeCurrentSupportEntry($errorEntries, $currentEntry);
127 fclose($handle);
128
129 return array(
130 'readable' => true,
131 'error_entries' => $errorEntries,
132 'recent_lines' => $recentLines,
133 );
134 }
135
136 /**
137 * @param array<int, array<int, string>> $errorEntries
138 * @param array<int, string> $recentLines
139 * @param array<int, string> $currentEntry
140 * @param bool $collectingEntry
141 * @return void
142 */
143 private function collectSupportExcerptLine(
144 string $line,
145 array &$errorEntries,
146 array &$recentLines,
147 array &$currentEntry,
148 bool &$collectingEntry
149 ): void {
150 $this->rememberRecentLine($recentLines, $line);
151
152 if ($this->isReportableWarningOrError($line)) {
153 if ($collectingEntry) {
154 $this->storeCurrentSupportEntry($errorEntries, $currentEntry);
155 }
156 $currentEntry = array($line);
157 $collectingEntry = true;
158 return;
159 }
160
161 if ($collectingEntry && $this->isContinuationLine($line)) {
162 $currentEntry[] = $line;
163 return;
164 }
165
166 if ($collectingEntry) {
167 $this->storeCurrentSupportEntry($errorEntries, $currentEntry);
168 }
169 $collectingEntry = false;
170 $currentEntry = array();
171 }
172
173 /**
174 * @param array<int, string> $recentLines
175 * @return void
176 */
177 private function rememberRecentLine(array &$recentLines, string $line): void {
178 $recentLines[] = $line;
179 if (count($recentLines) > 20) {
180 array_shift($recentLines);
181 }
182 }
183
184 private function isReportableWarningOrError(string $line): bool {
185 $hasError = stripos($line, '(ERROR)') !== false;
186 $hasWarn = stripos($line, '(WARN)') !== false;
187 $isDeleteError = stripos($line, 'SQL query error: DELETE command denied to user') !== false;
188 return ($hasError || $hasWarn) && !$isDeleteError;
189 }
190
191 private function isContinuationLine(string $line): bool {
192 $f = abj_service('functions');
193 return !$f->regexMatch("^\d{4}[-]\d{2}[-]\d{2} .*\(\w+\):\s.*$", $line);
194 }
195
196 /**
197 * @param array<int, array<int, string>> $errorEntries
198 * @param array<int, string> $currentEntry
199 * @return void
200 */
201 private function storeCurrentSupportEntry(array &$errorEntries, array &$currentEntry): void {
202 if (empty($currentEntry)) {
203 return;
204 }
205 $errorEntries[] = $currentEntry;
206 if (count($errorEntries) > 15) {
207 array_shift($errorEntries);
208 }
209 $currentEntry = array();
210 }
211
212 /**
213 * @param array<int, array<int, string>> $errorEntries
214 * @param array<int, string> $recentLines
215 * @return string
216 */
217 private function formatSupportExcerpt(array $errorEntries, array $recentLines): string {
218 if (empty($errorEntries)) {
219 if (empty($recentLines)) {
220 return "Log file is empty";
221 }
222 $output = "No ERROR/WARN entries found. Last " . count($recentLines) . " log lines:\n\n";
223 $output .= implode("", $recentLines);
224 return trim($output);
225 }
226
227 $output = "Last " . count($errorEntries) . " ERROR/WARN entries:\n\n";
228 foreach ($errorEntries as $entry) {
229 $output .= implode("\n", $entry) . "\n\n";
230 }
231
232 if (!empty($recentLines)) {
233 $output .= "Recent context (last " . count($recentLines) . " lines):\n\n";
234 $output .= implode("", $recentLines);
235 }
236
237 return trim($output);
238 }
239 }
240