url => microtime the request started */ private static array $started = []; public static function boot(): void { add_filter('pre_http_request', [self::class, 'start'], 1, 3); add_filter('pre_http_request', [self::class, 'blocked'], PHP_INT_MAX, 3); add_action('http_api_debug', [self::class, 'capture'], 10, 5); } /** * @param mixed $pre * @param array $args * @return mixed */ public static function start($pre, $args, $url) { if (is_string($url) && self::service($url) !== '') { self::$started[$url] = microtime(true); } return $pre; } /** * A request another plugin stopped before it left (it never reaches http_api_debug). * * @param mixed $pre * @param array $args * @return mixed */ public static function blocked($pre, $args, $url) { if ($pre instanceof \WP_Error && is_string($url) && self::service($url) !== '') { self::capture($pre, 'response', '', is_array($args) ? $args : [], $url); } return $pre; } /** * @param array|\WP_Error $response * @param array $args */ public static function capture($response, $context, $class, $args, $url): void { if ($context !== 'response' || !is_string($url)) { return; } $service = self::service($url); if ($service === '') { return; } $status = is_wp_error($response) ? 0 : (int) wp_remote_retrieve_response_code($response); $ms = isset(self::$started[$url]) ? (int) round((microtime(true) - self::$started[$url]) * 1000) : 0; unset(self::$started[$url]); if (!is_wp_error($response) && $status < 400) { return; } $code = ''; $message = ''; $ray = ''; if (is_wp_error($response)) { $code = (string) $response->get_error_code(); $message = $response->get_error_message(); } else { $ray = (string) wp_remote_retrieve_header($response, 'cf-ray'); $body = json_decode((string) wp_remote_retrieve_body($response), true); if (is_array($body) && isset($body['errors'][0]) && is_array($body['errors'][0])) { $code = (string) ($body['errors'][0]['code'] ?? ''); $message = (string) ($body['errors'][0]['message'] ?? ''); } elseif (is_array($body) && isset($body['status']['error'])) { $message = (string) $body['status']['error']; // Qdrant } } self::record([ 'time' => time(), 'feature' => TaskContext::current(), 'service' => $service, 'path' => self::path($url), 'model' => self::model($url, $args), 'status' => $status, 'code' => mb_substr($code, 0, 60), 'message' => mb_substr(Redact::text($message), 0, 200), 'ray' => mb_substr($ray, 0, 40), 'ms' => $ms, ]); } /** * @param array{time: int, feature: string, service: string, path: string, model: string, status: int, code: string, message: string, ray: string, ms: int} $entry */ public static function record(array $entry): void { $all = self::all(); array_unshift($all, $entry); $since = time() - self::KEEP; $all = array_values(array_filter($all, static fn(array $e): bool => (int) ($e['time'] ?? 0) >= $since)); update_option(self::OPTION, array_slice($all, 0, self::MAX), false); } /** * Newest first. * * @return list */ public static function all(): array { $all = get_option(self::OPTION, []); return is_array($all) ? array_values(array_filter($all, 'is_array')) : []; } public static function countSince(int $seconds): int { $since = time() - $seconds; return count(array_filter(self::all(), static fn(array $e): bool => (int) ($e['time'] ?? 0) >= $since)); } public static function clear(): void { delete_option(self::OPTION); } /** * Which service a URL belongs to, or '' for one this log does not watch. */ public static function service(string $url): string { $host = strtolower((string) wp_parse_url($url, PHP_URL_HOST)); if ($host === 'gateway.ai.cloudflare.com') { return 'gateway'; } if ($host === 'challenges.cloudflare.com') { return 'turnstile'; } if ($host === 'api.cloudflare.com') { return strpos($url, '/vectorize/') !== false ? 'vectorize' : 'workers-ai'; } $qdrant = (string) (get_plugin_settings()['qdrant_url'] ?? ''); if ($qdrant !== '' && strpos($url, untrailingslashit($qdrant)) === 0) { return 'qdrant'; } return ''; } /** * The address path, without the host, the query or the account ID. */ public static function path(string $url): string { $path = (string) wp_parse_url($url, PHP_URL_PATH); $account = get_account_id(); if ($account !== '') { $path = str_replace($account, '{account}', $path); } return (string) preg_replace('#/accounts/[0-9a-f]{32}#i', '/accounts/{account}', $path); } /** * The model a request was for: in the path (/ai/run/@cf/...) or in the JSON body. * * @param array $args */ private static function model(string $url, array $args): string { if (preg_match('#/ai/run/(@[^?]+)#', $url, $m)) { return mb_substr($m[1], 0, 100); } $body = isset($args['body']) && is_string($args['body']) ? json_decode($args['body'], true) : null; return is_array($body) && isset($body['model']) && is_string($body['model']) ? mb_substr($body['model'], 0, 100) : ''; } }