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 / database / DatabaseQueryDiagnostics.php

DatabaseQueryDiagnostics.php in 404 Solution 4.3.0, at includes/database/DatabaseQueryDiagnostics.php

197 lines 7.2 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 /**
8 * Query diagnostics for safe source labels, latency simulation, and logging.
9 *
10 * This component owns diagnostic behavior around a query execution without
11 * deciding whether the query should run or how errors recover. The executor
12 * calls it for simulated latency, query-budget instrumentation, safe SQL
13 * source labels, and malformed wpdb result visibility.
14 */
15 class ABJ_404_Solution_DatabaseQueryDiagnostics {
16
17 /** @var ABJ_404_Solution_Logging */
18 private $logger;
19
20 /** @param ABJ_404_Solution_Logging $logger */
21 public function __construct($logger) {
22 $this->logger = $logger;
23 }
24
25 /**
26 * @param string $query
27 * @param float $elapsedMs
28 * @param int $timeoutSeconds
29 * @return void
30 */
31 public function recordQueryBudgetIfEnabled(string $query, float $elapsedMs, int $timeoutSeconds): void {
32 if (function_exists('abj404_benchmark_record_db_query')) {
33 abj404_benchmark_record_db_query($elapsedMs);
34 }
35 if (function_exists('abj404_query_budget_record')
36 && class_exists('ABJ_404_Solution_QueryBudgetInstrumentation', false)
37 && ABJ_404_Solution_QueryBudgetInstrumentation::isEnabled()) {
38 abj404_query_budget_record($this->extractSqlFilename($query), $elapsedMs, $timeoutSeconds);
39 }
40 }
41
42 /**
43 * @param string $query
44 * @param mixed $rows
45 * @return void
46 */
47 public function logMalformedRowsIfNeeded(string $query, $rows): void {
48 if (is_array($rows)) {
49 return;
50 }
51 $sqlInfo = (defined('WP_DEBUG') && WP_DEBUG) ? $query : $this->extractSqlFilename($query);
52 $this->logger->errorMessage(
53 "Query result is not an array. Query: " . $sqlInfo,
54 new Exception("Query result is not an array.") // allow-raw-error: behavior preserved from pre-extraction DatabaseCore; passed to logger as diagnostic context, not thrown
55 );
56 }
57
58 /**
59 * Resolve a stable source identifier for safe logging.
60 *
61 * @param string $query
62 * @return string
63 */
64 public function extractSqlFilename($query) {
65 if (is_string($query) && $query !== '') {
66 if (preg_match('/\/\*\s*abj404:src=([A-Za-z0-9_:#.\\\\\-]+)\s*\*\//i', $query, $m)) {
67 return $m[1];
68 }
69 if (preg_match('/\/\*\s*-+\s*(.+?\.sql)\s+BEGIN\s*-+\s*\*\//i', $query, $m)) {
70 return basename($m[1]);
71 }
72 }
73 return $this->resolveCallerFromBacktrace();
74 }
75
76 /** @return string */
77 public function resolveCallerFromBacktrace() {
78 static $internalMethods = array(
79 'extractSqlFilename' => true,
80 'resolveCallerFromBacktrace' => true,
81 'queryAndGetResults' => true,
82 'attemptInvalidDataRetry' => true,
83 'attemptMissingTableRepairAndRetry' => true,
84 'repairCorruptedTableAndRetry' => true,
85 'recoverFromCollationMismatchAndRetry' => true,
86 'call_user_func_array' => true,
87 'call_user_func' => true,
88 '__call' => true,
89 );
90 $frames = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 40);
91 foreach ($frames as $frame) {
92 if ($this->shouldSkipBacktraceFrame($frame, $internalMethods)) {
93 continue;
94 }
95 return $this->formatBacktraceSource($frame);
96 }
97 return 'unknown-source';
98 }
99
100 /**
101 * @param array<string, mixed> $frame
102 * @param array<string, bool> $internalMethods
103 * @return bool
104 */
105 private function shouldSkipBacktraceFrame(array $frame, array $internalMethods): bool {
106 $fn = isset($frame['function']) && is_string($frame['function']) ? $frame['function'] : '';
107 if ($fn === '' || isset($internalMethods[$fn]) || strpos($fn, '{closure') !== false) {
108 return true;
109 }
110
111 $cls = isset($frame['class']) && is_string($frame['class']) ? $frame['class'] : '';
112 if ($cls !== '' && (strpos($cls, 'Patchwork') !== false || strpos($cls, 'PHPUnit\\') === 0)) {
113 return true;
114 }
115 if (strpos($fn, 'Patchwork\\') !== false) {
116 return true;
117 }
118
119 $fullFile = isset($frame['file']) && is_string($frame['file']) ? $frame['file'] : '';
120 return $fullFile !== '' && (strpos($fullFile, '/patchwork/') !== false || strpos($fullFile, '\\patchwork\\') !== false);
121 }
122
123 /**
124 * @param array<string, mixed> $frame
125 * @return string
126 */
127 private function formatBacktraceSource(array $frame): string {
128 $fn = isset($frame['function']) && is_string($frame['function']) ? $frame['function'] : 'unknown-source';
129 $cls = isset($frame['class']) && is_string($frame['class']) ? $frame['class'] : '';
130 $fileLabel = $this->fileLabelFromBacktraceFrame($frame);
131
132 if ($this->isInternalDatabaseClass($cls) && $fileLabel !== '' && !$this->isInternalDatabaseFileLabel($fileLabel)) {
133 return $fileLabel . '::' . $fn;
134 }
135 if ($cls !== '') {
136 return $this->shortClassLabel($cls) . '::' . $fn;
137 }
138 if ($fileLabel !== '') {
139 return $fileLabel . '::' . $fn;
140 }
141 return $fn;
142 }
143
144 /**
145 * @param array<string, mixed> $frame
146 * @return string
147 */
148 private function fileLabelFromBacktraceFrame(array $frame): string {
149 $fullFile = isset($frame['file']) && is_string($frame['file']) ? $frame['file'] : '';
150 $file = $fullFile !== '' ? basename($fullFile) : '';
151 $fileLabel = preg_replace('/\.php$/i', '', $file);
152 return is_string($fileLabel) ? $fileLabel : $file;
153 }
154
155 /** @param string $className @return bool */
156 private function isInternalDatabaseClass(string $className): bool {
157 return $className === 'ABJ_404_Solution_DatabaseCore'
158 || $className === 'ABJ_404_Solution_DatabaseQueryExecutor'
159 || $className === 'ABJ_404_Solution_DatabaseQueryDiagnostics'
160 || $className === 'ABJ_404_Solution_DataAccess';
161 }
162
163 /** @param string $fileLabel @return bool */
164 private function isInternalDatabaseFileLabel(string $fileLabel): bool {
165 return $fileLabel === 'DatabaseCore'
166 || $fileLabel === 'DatabaseQueryExecutor'
167 || $fileLabel === 'DatabaseQueryDiagnostics'
168 || $fileLabel === 'DataAccess';
169 }
170
171 /** @param string $className @return string */
172 private function shortClassLabel(string $className): string {
173 $shortClass = $className;
174 $nsPos = strrpos($shortClass, '\\');
175 if ($nsPos !== false) {
176 $shortClass = substr($shortClass, $nsPos + 1);
177 }
178 if (strpos($shortClass, 'ABJ_404_Solution_') === 0) {
179 return substr($shortClass, strlen('ABJ_404_Solution_'));
180 }
181 return $shortClass;
182 }
183
184 /** @return void */
185 public function applyDiagnosticLatencyIfConfigured(): void {
186 if (!function_exists('abj404_get_simulated_db_latency_ms')) {
187 return;
188 }
189 $delayMs = absint(abj404_get_simulated_db_latency_ms());
190 if ($delayMs <= 0) {
191 return;
192 }
193 $delayMs = min(5000, $delayMs);
194 usleep($delayMs * 1000);
195 }
196 }
197