vendor/torfs-ict/code-monitoring-bundle/src/ApiWriter/ApiWriter.php line 172

Open in your IDE?
  1. <?php
  2. declare(strict_types=1);
  3. namespace TorfsICT\Bundle\CodeMonitoringBundle\ApiWriter;
  4. use Symfony\Component\Console\Debug\CliRequest;
  5. use Symfony\Component\Console\Event\ConsoleTerminateEvent;
  6. use Symfony\Component\HttpFoundation\Request;
  7. use Symfony\Component\HttpFoundation\RequestStack;
  8. use Symfony\Component\HttpFoundation\Response;
  9. use Symfony\Component\HttpKernel\Event\TerminateEvent;
  10. use Symfony\Component\HttpKernel\Profiler\Profile;
  11. use Symfony\Component\HttpKernel\Profiler\Profiler;
  12. use Symfony\Component\Stopwatch\Stopwatch;
  13. use Symfony\Contracts\HttpClient\HttpClientInterface;
  14. use TorfsICT\Bundle\CodeMonitoringBundle\Exception\CaughtException;
  15. use TorfsICT\Bundle\CodeMonitoringBundle\ExceptionRenderer\ExceptionRenderer;
  16. final class ApiWriter
  17. {
  18.     private readonly string $url;
  19.     private readonly bool $useSpool;
  20.     /**
  21.      * @var \SplObjectStorage<\Throwable, \Throwable>
  22.      */
  23.     private \SplObjectStorage $queue;
  24.     public function __construct(
  25.         private readonly HttpClientInterface $httpClient,
  26.         private readonly ExceptionRenderer $renderer,
  27.         private readonly ?RequestStack $requestStack,
  28.         private readonly ?Stopwatch $stopwatch,
  29.         private readonly ?Profiler $profiler,
  30.         private readonly string $endpoint,
  31.         private readonly string $project,
  32.         private readonly string $environment,
  33.         private readonly string $secret,
  34.         private readonly ?string $spool,
  35.     ) {
  36.         $this->url sprintf('%s/monitoring'$this->endpoint);
  37.         $this->useSpool null !== $this->spool;
  38.         $this->queue = new \SplObjectStorage();
  39.         if ($this->useSpool && !is_dir((string) $this->spool)) {
  40.             throw new \RuntimeException(sprintf('Spool directory "%s" does not exist.'$this->spool));
  41.         }
  42.     }
  43.     public function exception(\Throwable $throwable): void
  44.     {
  45.         $this->queue->attach($throwable);
  46.     }
  47.     public function deprecation(\Throwable $throwable): void
  48.     {
  49.         $json $this->toArray($throwablefalse);
  50.         $this->process('deprecation'$json);
  51.     }
  52.     /**
  53.      * @param array<string, mixed> $json
  54.      */
  55.     private function process(string $type, array $json): void
  56.     {
  57.         $this->useSpool $this->queue($type$json) : $this->post($this->url.'/'.$type$json);
  58.     }
  59.     /**
  60.      * @return array<string, mixed>
  61.      */
  62.     private function toArray(\Throwable $throwablebool $includeDetails): array
  63.     {
  64.         $array = [
  65.             'file' => $throwable->getFile(),
  66.             'line' => $throwable->getLine(),
  67.             'user' => $includeDetails $this->renderer->getUserIdentifier() : null,
  68.             'message' => $throwable->getMessage(),
  69.             'contents' => $this->renderer->render($throwable$includeDetails),
  70.         ];
  71.         return $array;
  72.     }
  73.     /**
  74.      * @param array<string, mixed> $json
  75.      */
  76.     private function queue(string $type, array $json): void
  77.     {
  78.         // Use a hash of the array to create the filename to prevent duplicate files
  79.         $hash md5(serialize([
  80.             $json['file'],
  81.             $json['line'],
  82.             $json['message'],
  83.         ]));
  84.         $path sprintf('%s/spool.%s-%s'$this->spool$type$hash);
  85.         if (!file_exists($path)) {
  86.             file_put_contents($pathjson_encode($json));
  87.         }
  88.     }
  89.     /**
  90.      * @param array<string, mixed> $json
  91.      */
  92.     private function post(string $url, array $json): void
  93.     {
  94.         $json array_merge($json, [
  95.             'project' => $this->project,
  96.             'environment' => $this->environment,
  97.             'secret' => $this->secret,
  98.         ]);
  99.         $this->httpClient->request('POST'$url, [
  100.             'json' => $json,
  101.             'headers' => [
  102.                 'Content-Type' => 'application/ld+json',
  103.             ],
  104.         ]);
  105.     }
  106.     public function sendSpool(): void
  107.     {
  108.         if (!$this->useSpool) {
  109.             return;
  110.         }
  111.         assert(is_string($this->spool));
  112.         foreach ((new \DirectoryIterator($this->spool)) as $file) {
  113.             /** @var \DirectoryIterator $file */
  114.             if (!$file->isFile()) {
  115.                 continue;
  116.             }
  117.             $basename $file->getBasename();
  118.             if (fnmatch('*.sending'$basename)) {
  119.                 continue;
  120.             }
  121.             $type preg_filter('/spool\.([^-]+)-.+/''$1'$basename);
  122.             if (!in_array($type, ['deprecation''exception'], true)) {
  123.                 continue;
  124.             }
  125.             $seen $file->getCTime();
  126.             $renamed $file->getPathname().'.sending';
  127.             rename($file->getPathname(), $renamed);
  128.             /** @var array<string, mixed> $json */
  129.             $json json_decode((string) file_get_contents($renamed), true);
  130.             if (false !== $seen) {
  131.                 $seen \DateTimeImmutable::createFromFormat('U', (string) $seen);
  132.                 if ($seen instanceof \DateTimeImmutable) {
  133.                     $json['_seen'] = $seen->format(\DateTimeInterface::ATOM);
  134.                 }
  135.             }
  136.             $this->post($this->url.'/'.$type$json);
  137.             unlink($renamed);
  138.         }
  139.     }
  140.     public function onKernelTerminate(TerminateEvent $event): void
  141.     {
  142.         $this->onTerminate($event->getRequest(), $event->getResponse());
  143.     }
  144.     public function onConsoleTerminate(ConsoleTerminateEvent $event): void
  145.     {
  146.         $request $this->requestStack?->getCurrentRequest();
  147.         if (!$request instanceof CliRequest || $request->command !== $event->getCommand()) {
  148.             return;
  149.         }
  150.         if (null !== $sectionId $request->attributes->get('_stopwatch_token')) {
  151.             // we must close the section before saving the profile to allow late collect
  152.             try {
  153.                 assert(is_string($sectionId));
  154.                 $this->stopwatch?->stopSection($sectionId);
  155.             } catch (\LogicException) {
  156.                 // noop
  157.             }
  158.         }
  159.         $request->command->exitCode $event->getExitCode();
  160.         $request->command->interruptedBySignal $event->getInterruptingSignal();
  161.         $this->onTerminate($request$request->getResponse());
  162.     }
  163.     private function onTerminate(Request $requestResponse $response): void
  164.     {
  165.         foreach ($this->queue as $throwable) {
  166.             $json $this->toArray($throwabletrue);
  167.             $json['caught'] = $throwable instanceof CaughtException;
  168.             if (null !== $this->profiler) {
  169.                 $profile $this->profiler->collect($request$response$throwable);
  170.                 if ($profile instanceof Profile) {
  171.                     $this->profiler->saveProfile($profile);
  172.                     $json['token'] = $profile->getToken();
  173.                 }
  174.             }
  175.             $this->process('exception'$json);
  176.             $this->queue->detach($throwable);
  177.         }
  178.     }
  179. }