| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139 |
- <?php
- namespace Webkul\BagistoApi\Http\Middleware;
- use Closure;
- use Illuminate\Http\Request;
- use Illuminate\Support\Facades\Log;
- use Webkul\BagistoApi\Jobs\LogApiRequestJob;
- /**
- * Logs all API requests for security audit trail and monitoring
- */
- class LogApiRequests
- {
- /**
- * Paths to exclude from logging
- */
- private const EXCLUDED_PATHS = [
- 'health',
- 'ping',
- 'docs',
- 'graphiql',
- 'swagger-ui',
- 'docs.json',
- '.well-known',
- ];
- public function handle(Request $request, Closure $next)
- {
- $startTime = microtime(true);
- $response = $next($request);
- $duration = round((microtime(true) - $startTime) * 1000, 2);
- if ($this->shouldLog($request, $response)) {
- $this->logRequestAsync($request, $response, $duration);
- }
- return $response;
- }
- private function shouldLog(Request $request, $response): bool
- {
- foreach (self::EXCLUDED_PATHS as $path) {
- if (str_contains($request->path(), $path)) {
- return false;
- }
- }
- return str_starts_with($request->path(), 'api/');
- }
- private function logRequestAsync(Request $request, $response, float $duration): void
- {
- $logData = [
- 'method' => $request->getMethod(),
- 'path' => $request->path(),
- 'status' => $response->getStatusCode(),
- 'duration_ms' => $duration,
- 'ip' => $request->ip(),
- 'user_agent' => substr($request->userAgent() ?? '', 0, 255),
- 'user_id' => auth()->id(),
- 'api_key' => $this->maskApiKey($request->header('X-STOREFRONT-KEY')),
- 'graphql_operation' => $this->getGraphQLOperation($request),
- ];
- try {
- // Skip async logging in testing environment to avoid job queue issues
- if (app()->environment('testing')) {
- $this->logSync($logData);
- } elseif (class_exists(LogApiRequestJob::class)) {
- dispatch(new LogApiRequestJob($logData))->onQueue('api-logs');
- } else {
- $this->logSync($logData);
- }
- } catch (\Throwable $e) {
- $this->logSync($logData);
- }
- }
- private function logSync(array $logData): void
- {
- $level = $this->getLogLevel($logData['status']);
-
- try {
- // Use 'api' channel if configured, otherwise fallback to default
- if (config('logging.channels.api')) {
- Log::channel('api')->log($level, 'API Request', $logData);
- } else {
- Log::log($level, 'API Request', $logData);
- }
- } catch (\Throwable $e) {
- // If channel logging fails, use default logger
- Log::log($level, 'API Request', $logData);
- }
- }
- private function getLogLevel(int $statusCode): string
- {
- if ($statusCode >= 500) {
- return 'error';
- }
- if ($statusCode >= 400) {
- return 'warning';
- }
- return 'info';
- }
- private function maskApiKey(?string $apiKey): string
- {
- if (! $apiKey || strlen($apiKey) < 6) {
- return 'none';
- }
- return substr($apiKey, 0, 3).'***'.substr($apiKey, -3);
- }
- private function getGraphQLOperation(Request $request): ?string
- {
- if ($request->path() !== 'api/graphql') {
- return null;
- }
- $input = $request->json('operationName')
- ?? $this->extractOperationName($request->json('query') ?? '');
- return $input;
- }
- private function extractOperationName(string $query): ?string
- {
- if (preg_match('/^\s*(query|mutation|subscription)\s+(\w+)/i', $query, $matches)) {
- return strtolower($matches[1]).': '.$matches[2];
- }
- return null;
- }
- }
|