src/EventSubscriber/ApiRequestLogSubscriber.php line 67

Open in your IDE?
  1. <?php
  2. namespace App\EventSubscriber;
  3. use Psr\Log\LoggerInterface;
  4. use Symfony\Component\EventDispatcher\EventSubscriberInterface;
  5. use Symfony\Component\HttpKernel\Event\ControllerEvent;
  6. use Symfony\Component\HttpKernel\Event\ResponseEvent;
  7. use Symfony\Component\HttpKernel\KernelEvents;
  8. use Symfony\Component\HttpFoundation\Request;
  9. use Symfony\Component\HttpFoundation\Response;
  10. class ApiRequestLogSubscriber implements EventSubscriberInterface
  11. {
  12. private const IGNORED_ROUTES = [
  13. 'app_api_overall_saleorder_getsaleorders',
  14. ];
  15. public function __construct(private readonly LoggerInterface $logger)
  16. {
  17. }
  18. public static function getSubscribedEvents(): array
  19. {
  20. return [
  21. KernelEvents::CONTROLLER => 'onKernelController',
  22. KernelEvents::RESPONSE => 'onKernelResponse',
  23. ];
  24. }
  25. public function onKernelController(ControllerEvent $event): void
  26. {
  27. if (!$event->isMainRequest()) {
  28. return;
  29. }
  30. $controller = $event->getController();
  31. if (is_array($controller)) {
  32. $controllerClass = get_class($controller[0]);
  33. $controllerMethod = $controller[1];
  34. } elseif (is_object($controller)) {
  35. $controllerClass = get_class($controller);
  36. $controllerMethod = '__invoke';
  37. } else {
  38. return;
  39. }
  40. if (!str_starts_with($controllerClass, 'App\\Controller\\API\\')) {
  41. return;
  42. }
  43. $request = $event->getRequest();
  44. if ($this->shouldIgnoreRoute($request)) {
  45. return;
  46. }
  47. $this->logger->info('API call', [
  48. 'route' => $request->attributes->get('_route'),
  49. 'method' => $request->getMethod(),
  50. 'path' => $request->getPathInfo(),
  51. 'query' => $request->query->all(),
  52. 'controller' => $controllerClass . '::' . $controllerMethod
  53. ]);
  54. }
  55. public function onKernelResponse(ResponseEvent $event): void
  56. {
  57. if (!$event->isMainRequest()) {
  58. return;
  59. }
  60. $request = $event->getRequest();
  61. if ($this->shouldIgnoreRoute($request)) {
  62. return;
  63. }
  64. if (!$this->isApiController($request)) {
  65. return;
  66. }
  67. $controller = $request->attributes->get('_controller');
  68. if (!is_string($controller)) {
  69. return;
  70. }
  71. $response = $event->getResponse();
  72. $statusCode = $response->getStatusCode();
  73. $context = [
  74. 'route' => $request->attributes->get('_route'),
  75. 'method' => $request->getMethod(),
  76. 'path' => $request->getPathInfo(),
  77. 'controller' => $controller,
  78. 'status_code' => $statusCode,
  79. ];
  80. if ($statusCode >= 400) {
  81. $errorMessage = $this->extractErrorMessage($response);
  82. if ($errorMessage !== null) {
  83. $context['error_message'] = $errorMessage;
  84. }
  85. $this->logger->error('API response', $context);
  86. return;
  87. }
  88. $this->logger->info('API response', $context);
  89. }
  90. private function isApiController(Request $request): bool
  91. {
  92. $controller = $request->attributes->get('_controller');
  93. if (is_array($controller)) {
  94. $controller = get_class($controller[0]) . '::' . $controller[1];
  95. } elseif (is_object($controller)) {
  96. $controller = get_class($controller) . '::__invoke';
  97. }
  98. return is_string($controller) && str_starts_with($controller, 'App\\Controller\\API\\');
  99. }
  100. private function extractErrorMessage(Response $response): ?string
  101. {
  102. $content = trim($response->getContent());
  103. if ($content === '') {
  104. return Response::$statusTexts[$response->getStatusCode()] ?? null;
  105. }
  106. $contentType = strtolower((string) $response->headers->get('content-type'));
  107. if (str_contains($contentType, 'application/json') || str_contains($contentType, '+json')) {
  108. $decoded = json_decode($content, true);
  109. if (json_last_error() === JSON_ERROR_NONE) {
  110. if (is_string($decoded)) {
  111. return $decoded;
  112. }
  113. if (is_array($decoded)) {
  114. foreach (['message', 'error_description', 'detail', 'title', 'error'] as $key) {
  115. $value = $decoded[$key] ?? null;
  116. if (is_string($value) && trim($value) !== '') {
  117. return $value;
  118. }
  119. }
  120. if (isset($decoded['error']) && is_array($decoded['error'])) {
  121. foreach (['message', 'error_description', 'detail', 'title', 'error'] as $key) {
  122. $value = $decoded['error'][$key] ?? null;
  123. if (is_string($value) && trim($value) !== '') {
  124. return $value;
  125. }
  126. }
  127. }
  128. }
  129. }
  130. }
  131. if (strlen($content) > 500) {
  132. return substr($content, 0, 500) . '...[truncated]';
  133. }
  134. return $content;
  135. }
  136. private function shouldIgnoreRoute(Request $request): bool
  137. {
  138. return in_array((string) $request->attributes->get('_route'), self::IGNORED_ROUTES, true);
  139. }
  140. }