PluginProbe
404 Solution / trunk
404 Solution vtrunk
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 / feedback / SupportLogExcerpt.php

SupportLogExcerpt.php in 404 Solution trunk, at includes/feedback/SupportLogExcerpt.php

132 lines 5.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 if (!defined('ABSPATH')) {
4 exit;
5 }
6
7 require_once __DIR__ . '/FeedbackTransportLog.php';
8
9 /**
10 * The sanitized debug-log tail for a report, or a statement of why there is
11 * none.
12 *
13 * Three collectors (the support request, its preview, and the uninstall
14 * feedback) each used to run the same fifteen lines: resolve the optional
15 * logging service, call getSanitizedLogExcerptForSupport(), bound it, and
16 * return '' from every branch where any of that did not work out. That empty
17 * string is the same defect the collection manifest exists to end, one layer
18 * up: a developer holding the report cannot tell "this site has no log" from
19 * "the logging service was not registered" from "reading the log threw".
20 * formatSnapshot() answers in words when the FILE is missing or unreadable
21 * ("No log file available"); the older repeated collectors lost that fact.
22 *
23 * So the resolution lives here once, and every branch that yields no excerpt
24 * yields a sentence instead. Three copies of a silent path could each be fixed
25 * three times; one shared path can only be fixed once.
26 */
27 final class ABJ_404_Solution_SupportLogExcerpt {
28
29 /** Prefix every stated reason carries, so a reader can grep for them. */
30 const REASON_PREFIX = '[404 Solution] Sanitized debug log unavailable: ';
31
32 /** Bytes of an underlying exception message carried into the report. */
33 const MAX_REASON_DETAIL_LENGTH = 200;
34
35 /** Keeps the latest error header inside the 5 KB support preview. */
36 const LATEST_ERROR_MAX_BYTES = 4096;
37
38 /**
39 * Present one DebugLogReader snapshot without performing another read.
40 *
41 * @param array<string, mixed> $snapshot
42 */
43 public static function formatSnapshot(array $snapshot): string {
44 $status = isset($snapshot['status']) && is_string($snapshot['status'])
45 ? $snapshot['status'] : 'unreadable';
46 if ($status === 'missing') {
47 return 'No log file available';
48 }
49 if ($status !== 'ok') {
50 return 'Log file not readable';
51 }
52 $rawEntries = isset($snapshot['error_entries']) && is_array($snapshot['error_entries'])
53 ? $snapshot['error_entries'] : array();
54 $entries = array();
55 foreach ($rawEntries as $rawEntry) {
56 if (is_array($rawEntry)) {
57 $lines = array_values(array_filter($rawEntry, 'is_string'));
58 if ($lines !== array()) {
59 $entries[] = $lines;
60 }
61 }
62 }
63 $rawRecent = isset($snapshot['recent_lines']) && is_array($snapshot['recent_lines'])
64 ? $snapshot['recent_lines'] : array();
65 $recent = array_values(array_filter($rawRecent, 'is_string'));
66 if ($entries === array()) {
67 if ($recent === array()) {
68 return 'Log file is empty';
69 }
70 return trim('No ERROR/WARN entries found. Last ' . count($recent)
71 . " log lines:\n\n" . implode('', $recent));
72 }
73
74 $output = 'Last ' . count($entries) . " ERROR/WARN entries:\n\n";
75 foreach ($entries as $entry) {
76 $output .= implode("\n", $entry) . "\n\n";
77 }
78 if ($recent !== array()) {
79 $output .= 'Recent context (last ' . count($recent) . " lines):\n\n"
80 . implode('', $recent);
81 }
82 $latest = isset($snapshot['line']) && is_string($snapshot['line']) ? $snapshot['line'] : '';
83 if ($latest !== '') {
84 $bounded = substr($latest, 0, self::LATEST_ERROR_MAX_BYTES);
85 $output .= "\n\nLatest reportable ERROR evidence:\n\n" . $bounded;
86 if (strlen($latest) > strlen($bounded)) {
87 $output .= "\n[404 Solution] Latest ERROR evidence truncated to "
88 . self::LATEST_ERROR_MAX_BYTES . ' bytes.';
89 }
90 }
91 return trim($output);
92 }
93
94 /**
95 * The excerpt, tail-bounded, or a one-line reason.
96 *
97 * @param string $context Which collector is asking, for the warning log.
98 * @param int $maxBytes Tail bound for the excerpt itself; 0 for unbounded.
99 * @return string Never empty.
100 */
101 public static function resolve(string $context, int $maxBytes = 0): string {
102 if (!function_exists('abj_service_optional')) {
103 return self::REASON_PREFIX . 'the plugin service container is not loaded in this request.';
104 }
105 $logger = abj_service_optional('logging');
106 if (!is_object($logger) || !method_exists($logger, 'getSanitizedLogExcerptForSupport')) {
107 return self::REASON_PREFIX . 'the logging service is not available on this install.';
108 }
109 try {
110 $excerpt = $logger->getSanitizedLogExcerptForSupport();
111 if (!is_string($excerpt)) {
112 return self::REASON_PREFIX . 'the logging service returned no readable excerpt.';
113 }
114 if ($excerpt === '') {
115 return self::REASON_PREFIX . 'the log excerpt came back empty.';
116 }
117 return $maxBytes > 0 && strlen($excerpt) > $maxBytes
118 ? substr($excerpt, -$maxBytes) : $excerpt;
119 } catch (\Throwable $e) {
120 ABJ_404_Solution_FeedbackTransportLog::log(
121 'warn',
122 $context . ' debug-log excerpt unavailable: ' . $e->getMessage()
123 );
124 // The underlying message is carried, not just logged: the site that
125 // hit this is the one site whose log we cannot read, so the debug
126 // log is exactly where this explanation would NOT reach anyone.
127 return self::REASON_PREFIX . 'reading it failed ('
128 . substr($e->getMessage(), 0, self::MAX_REASON_DETAIL_LENGTH) . ').';
129 }
130 }
131 }
132