shouldSkip($request)) { return $next($request); } $startTime = microtime(true); $response = $next($request); $this->logRequest($request, $response, $startTime); return $response; } protected function shouldSkip(Request $request): bool { foreach ($this->exceptPaths as $pattern) { if ($request->is($pattern)) { return true; } } return false; } protected function logRequest(Request $request, Response $response, float $startTime): void { try { $durationMs = (int) ((microtime(true) - $startTime) * 1000); $logData = [ 'method' => $request->method(), 'url' => $request->fullUrl(), 'route_name' => $request->route()?->getName(), 'request_headers' => $this->maskSensitiveHeaders($request->headers->all()), 'request_body' => $this->maskSensitiveFields($request->all()), 'request_query' => $request->query(), 'response_status' => $response->getStatusCode(), 'response_body' => $this->truncateResponse($response->getContent()), 'duration_ms' => $durationMs, 'ip_address' => $request->ip(), 'user_agent' => $request->userAgent(), 'user_id' => $request->user()?->id, 'tenant_id' => $request->user()?->current_tenant_id ?? null, ]; // DB 저장 ApiRequestLog::create($logData); // 로그 파일 저장 $this->writeToLogFile($logData); } catch (\Throwable $e) { // 로깅 실패가 비즈니스 로직에 영향을 주지 않도록 조용히 처리 Log::error('API 로깅 실패', [ 'error' => $e->getMessage(), 'url' => $request->fullUrl(), ]); } } protected function maskSensitiveHeaders(array $headers): array { foreach ($this->sensitiveHeaders as $header) { if (isset($headers[$header])) { $headers[$header] = ['***MASKED***']; } } return $headers; } protected function maskSensitiveFields(array $data): array { foreach ($data as $key => $value) { if (in_array(strtolower($key), $this->sensitiveFields)) { $data[$key] = '***MASKED***'; } elseif (is_array($value)) { $data[$key] = $this->maskSensitiveFields($value); } } return $data; } protected function truncateResponse(?string $content): ?string { if ($content === null) { return null; } // 10KB 이상이면 잘라내기 $maxLength = 10 * 1024; if (strlen($content) > $maxLength) { return substr($content, 0, $maxLength) . '...[TRUNCATED]'; } return $content; } protected function writeToLogFile(array $logData): void { $logMessage = sprintf( "[%s] %s %s | Status: %d | Duration: %dms | IP: %s | User: %s", now()->format('Y-m-d H:i:s'), $logData['method'], $logData['url'], $logData['response_status'], $logData['duration_ms'], $logData['ip_address'] ?? 'N/A', $logData['user_id'] ?? 'guest' ); Log::channel('api')->info($logMessage, [ 'request_body' => $logData['request_body'], 'response_status' => $logData['response_status'], ]); } }