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 / diagnostics / PostAuthorizationFailureTracer.php

PostAuthorizationFailureTracer.php in 404 Solution trunk, at includes/diagnostics/PostAuthorizationFailureTracer.php

256 lines 8.6 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 * Durable attribution for AJAX failure work reached after authorization.
9 *
10 * The failure fingerprint is persisted before detail construction or service
11 * lookup. Every later blocking boundary carries the same failure id, while
12 * exception messages, log lines, paths, SQL, and response details remain out
13 * of the diagnostic journal.
14 */
15 final class ABJ_404_Solution_PostAuthorizationFailureTracer {
16
17 /** @var array<int, array{request_id:string,failure_id:string,branch:string}> */
18 private static $contexts = array();
19
20 /** @var int */
21 private static $operationSequence = 0;
22
23 /**
24 * Record the failure first, then trace detail construction and logging.
25 *
26 * @template T
27 * @param Throwable|null $throwable
28 * @param callable(): mixed $detailsFactory
29 * @param callable(mixed): T $logging
30 * @return T
31 */
32 public static function trace(
33 string $branch,
34 $throwable,
35 callable $detailsFactory,
36 callable $logging
37 ) {
38 $requestId = self::requestId();
39 if ($requestId === '') {
40 return $logging($detailsFactory());
41 }
42
43 $safeBranch = self::safeBranch($branch);
44 $failureId = self::operationId($requestId, $safeBranch);
45 $fields = array(
46 'operation_id' => $failureId,
47 'failure_id' => $failureId,
48 'branch' => $safeBranch,
49 );
50 if ($throwable instanceof Throwable) {
51 $fields['error'] = self::errorSummary($throwable);
52 }
53
54 ABJ_404_Solution_AjaxCheckpointLogger::recordFrequent(
55 $requestId,
56 'ajax_failure_branch',
57 $fields
58 );
59 ABJ_404_Solution_AjaxCheckpointLogger::recordFrequent(
60 $requestId,
61 'ajax_failure_log_start',
62 $fields
63 );
64 self::$contexts[] = array(
65 'request_id' => $requestId,
66 'failure_id' => $failureId,
67 'branch' => $safeBranch,
68 );
69 $startedAt = self::nowFloat();
70
71 try {
72 $details = self::aroundOperation('detail_construction', $detailsFactory);
73 $result = $logging($details);
74 } catch (Throwable $error) {
75 array_pop(self::$contexts);
76 ABJ_404_Solution_AjaxCheckpointLogger::recordFrequent(
77 $requestId,
78 'ajax_failure_log_end',
79 array_merge($fields, array(
80 'status' => 'error',
81 'elapsed_ms' => self::elapsedMilliseconds($startedAt),
82 'error' => self::errorSummary($error),
83 ))
84 );
85 throw $error;
86 }
87
88 array_pop(self::$contexts);
89 ABJ_404_Solution_AjaxCheckpointLogger::recordFrequent(
90 $requestId,
91 'ajax_failure_log_end',
92 array_merge($fields, array(
93 'status' => 'complete',
94 'elapsed_ms' => self::elapsedMilliseconds($startedAt),
95 ))
96 );
97 return $result;
98 }
99
100 /**
101 * Trace one failure-logging sub-operation while trace() is active.
102 *
103 * @template T
104 * @param callable(): T $work
105 * @return T
106 */
107 public static function aroundOperation(string $operation, callable $work) {
108 $context = self::activeContext();
109 if ($context === null) {
110 return $work();
111 }
112
113 $operationId = self::operationId($context['request_id'], $operation);
114 $fields = array(
115 'operation_id' => $operationId,
116 'failure_id' => $context['failure_id'],
117 'branch' => $context['branch'],
118 'operation' => self::safeOperation($operation),
119 );
120 ABJ_404_Solution_AjaxCheckpointLogger::recordFrequent(
121 $context['request_id'],
122 'ajax_failure_log_operation_start',
123 $fields
124 );
125 $startedAt = self::nowFloat();
126 try {
127 $result = $work();
128 } catch (Throwable $error) {
129 ABJ_404_Solution_AjaxCheckpointLogger::recordFrequent(
130 $context['request_id'],
131 'ajax_failure_log_operation_end',
132 array_merge($fields, array(
133 'status' => 'error',
134 'elapsed_ms' => self::elapsedMilliseconds($startedAt),
135 'error' => self::errorSummary($error),
136 ))
137 );
138 throw $error;
139 }
140
141 ABJ_404_Solution_AjaxCheckpointLogger::recordFrequent(
142 $context['request_id'],
143 'ajax_failure_log_operation_end',
144 array_merge($fields, array(
145 'status' => 'complete',
146 'elapsed_ms' => self::elapsedMilliseconds($startedAt),
147 'result' => self::resultSummary($result),
148 ))
149 );
150 return $result;
151 }
152
153 /**
154 * Map native Logging operations to explicit failure-path terminology.
155 *
156 * @template T
157 * @param callable(): T $work
158 * @return T
159 */
160 public static function aroundNativeOperation(string $operation, callable $work) {
161 $mapped = $operation === 'path_resolution'
162 ? 'native_path_resolution'
163 : ($operation === 'write' ? 'native_write_flush_return' : 'native_' . $operation);
164 return self::aroundOperation($mapped, $work);
165 }
166
167 public static function isActive(): bool {
168 return self::activeContext() !== null;
169 }
170
171 /** @return array{request_id:string,failure_id:string,branch:string}|null */
172 private static function activeContext(): ?array {
173 $context = end(self::$contexts);
174 return is_array($context) ? $context : null;
175 }
176
177 private static function requestId(): string {
178 if (!class_exists('ABJ_404_Solution_AjaxDiagnosticRequestPolicy')) {
179 return '';
180 }
181 return ABJ_404_Solution_AjaxDiagnosticRequestPolicy::instrumentedRequestIdFromGlobalContext();
182 }
183
184 private static function operationId(string $requestId, string $operation): string {
185 self::$operationSequence++;
186 return substr(hash(
187 'sha256',
188 $requestId . '|' . $operation . '|' . self::$operationSequence
189 ), 0, 12);
190 }
191
192 private static function safeBranch(string $branch): string {
193 if (in_array($branch, array('rate_limit', 'exception_caught', 'failure_branch'), true)) {
194 return $branch;
195 }
196 return 'branch#' . substr(hash('sha256', $branch), 0, 12);
197 }
198
199 private static function safeOperation(string $operation): string {
200 return preg_match('/^[a-z][a-z0-9_]{0,79}$/', $operation) === 1
201 ? $operation
202 : 'operation#' . substr(hash('sha256', $operation), 0, 12);
203 }
204
205 /** @return array<string, mixed> */
206 private static function errorSummary(Throwable $error): array {
207 $message = $error->getMessage();
208 $class = get_class($error);
209 return array(
210 'class' => preg_match('/^[A-Za-z_\\\\][A-Za-z0-9_\\\\]{0,159}$/', $class) === 1
211 ? $class
212 : 'class#' . substr(hash('sha256', $class), 0, 12),
213 'code' => is_int($error->getCode()) ? $error->getCode() : 0,
214 'message' => 'message#' . substr(hash('sha256', $message), 0, 12),
215 'message_length' => strlen($message),
216 );
217 }
218
219 /**
220 * @param mixed $result
221 * @return array{type:string,value?:bool|int|float|string|null}
222 */
223 private static function resultSummary($result): array {
224 if (is_bool($result) || is_int($result) || is_float($result) || $result === null) {
225 return array('type' => gettype($result), 'value' => $result);
226 }
227 if (!is_object($result)) {
228 return array('type' => gettype($result));
229 }
230 $class = get_class($result);
231 return array('type' => preg_match('/^[A-Za-z_\\\\][A-Za-z0-9_\\\\]{0,159}$/', $class) === 1
232 ? 'object:' . $class
233 : 'object:class#' . substr(hash('sha256', $class), 0, 12));
234 }
235
236 private static function nowFloat(): ?float {
237 if (function_exists('abj_clock')) {
238 return abj_clock()->nowFloat();
239 }
240 if (class_exists('ABJ_404_Solution_SystemClock')) {
241 return (new ABJ_404_Solution_SystemClock())->nowFloat();
242 }
243 return null;
244 }
245
246 private static function elapsedMilliseconds(?float $startedAt): ?int {
247 if ($startedAt === null) {
248 return null;
249 }
250 $finishedAt = self::nowFloat();
251 return $finishedAt === null
252 ? null
253 : max(0, (int)round(($finishedAt - $startedAt) * 1000));
254 }
255 }
256