path(), '/'); if (!$this->shouldLogPath($path)) { return $handler($request); } $start = microtime(true); $requestPayload = $this->buildRequestPayload($request); try { $response = $handler($request); } catch (Throwable $e) { $costMs = $this->costMs($start); Log::error('[InterfaceRequestLog] ' . $this->encodeJson([ 'path' => '/' . $path, 'method' => strtoupper($request->method()), 'ip' => $request->getRealIp(), 'cost_ms' => $costMs, 'request' => $requestPayload, 'success' => false, 'fail_reason' => $e->getMessage(), 'exception_class' => get_class($e), 'exception_trace' => $this->truncateText($e->getTraceAsString(), 3000), ])); throw $e; } $costMs = $this->costMs($start); $statusCode = method_exists($response, 'getStatusCode') ? intval($response->getStatusCode()) : 200; $responseBodyRaw = $this->extractResponseBody($response); $responseDecoded = $this->decodeJsonArray($responseBodyRaw); $success = false; $failReason = ''; $responseData = null; if (is_array($responseDecoded)) { if (array_key_exists('code', $responseDecoded)) { $success = intval($responseDecoded['code']) === 1; } else { $success = $statusCode >= 200 && $statusCode < 400; } $responseData = $responseDecoded['data'] ?? $responseDecoded; if (!$success) { $failReasonRaw = $responseDecoded['message'] ?? ($responseDecoded['msg'] ?? ''); $failReason = is_string($failReasonRaw) ? $failReasonRaw : strval($failReasonRaw); } } else { $success = $statusCode >= 200 && $statusCode < 400; if ($success) { $responseData = $this->truncateText($responseBodyRaw, 2000); } else { $failReason = $this->truncateText($responseBodyRaw, 2000); } } $logPayload = [ 'path' => '/' . $path, 'method' => strtoupper($request->method()), 'ip' => $request->getRealIp(), 'status_code' => $statusCode, 'cost_ms' => $costMs, 'request' => $requestPayload, 'success' => $success, ]; if ($success) { $logPayload['response_data'] = $this->maskMixed($responseData); Log::info('[InterfaceRequestLog] ' . $this->encodeJson($logPayload)); } else { $logPayload['fail_reason'] = $failReason !== '' ? $failReason : 'Unknown error'; if (is_array($responseDecoded)) { $logPayload['api_response'] = $this->maskMixed($responseDecoded); } Log::error('[InterfaceRequestLog] ' . $this->encodeJson($logPayload)); } return $response; } private function shouldLogPath(string $path): bool { foreach (self::API_PATH_PREFIXES as $prefix) { if (str_starts_with($path, $prefix)) { return true; } } return false; } private function buildRequestPayload(Request $request): array { $query = $request->get(); $post = $request->post(); if (!is_array($query)) { $query = []; } if (!is_array($post)) { $post = []; } $rawBody = method_exists($request, 'rawBody') ? strval($request->rawBody()) : ''; $jsonBody = []; if ($rawBody !== '') { $jsonDecoded = json_decode($rawBody, true); if (is_array($jsonDecoded)) { $jsonBody = $jsonDecoded; } } return [ 'query' => $this->maskMixed($query), 'post' => $this->maskMixed($post), 'json' => $this->maskMixed($jsonBody), ]; } private function extractResponseBody(Response $response): string { if (method_exists($response, 'rawBody')) { return strval($response->rawBody()); } return ''; } private function decodeJsonArray(string $content): ?array { if ($content === '') { return null; } $decoded = json_decode($content, true); return is_array($decoded) ? $decoded : null; } private function maskMixed($value, ?string $parentKey = null) { if (is_array($value)) { $result = []; foreach ($value as $k => $v) { $keyName = is_string($k) ? $k : $parentKey; $result[$k] = $this->maskMixed($v, $keyName); } return $result; } if ($parentKey !== null && $this->isSensitiveKey($parentKey)) { return '***'; } return $value; } private function isSensitiveKey(string $key): bool { $normalized = strtolower(trim($key)); foreach (self::SENSITIVE_KEYS as $sensitive) { if ($normalized === $sensitive) { return true; } if (str_contains($normalized, $sensitive)) { return true; } } return false; } private function truncateText(string $text, int $maxLen): string { if ($maxLen <= 0) { return ''; } if (mb_strlen($text) <= $maxLen) { return $text; } return mb_substr($text, 0, $maxLen) . '...'; } private function encodeJson(array $payload): string { return json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); } private function costMs(float $start): int { return intval((microtime(true) - $start) * 1000); } }