<?php
declare(strict_types=1);
namespace App\Shared\Infrastructure\EventSubscriber;
use App\Shared\Infrastructure\Observability\ObservabilityLogger;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\Event\TerminateEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Throwable;
/**
* FIX-260526-010 — [OBS-PERF-TRAMITE] medición wall-clock por request HTTP entrante.
*
* Captura t0 en kernel.request y dt total en kernel.terminate. Si dt > umbral
* (3.0s por defecto) emite warning permanente al canal de auditoría con
* {route, user, dt_ms}. Pensado para correlacionar con [OBS-PERF-HTTP] y
* detectar la operación concreta que dispara la espera de 5 minutos reportada
* por Enrique.
*
* Garantías:
* - Sólo lectura de Request; ningún side-effect en flujo funcional.
* - Logger envuelto en try/catch (`safeLog`); errores no propagan.
* - Sólo loggea sub-requests `main` (ignora ESI/fragments).
*/
final class RequestPerfSubscriber implements EventSubscriberInterface
{
private const ATTR_START_TIME = '_obs_perf_t0';
private float $thresholdSeconds;
public function __construct(
private ObservabilityLogger $observabilityLogger,
private ?TokenStorageInterface $tokenStorage = null,
float $thresholdSeconds = 3.0
) {
$this->thresholdSeconds = $thresholdSeconds;
}
public static function getSubscribedEvents(): array
{
return [
KernelEvents::REQUEST => ['onKernelRequest', 1024], // prioridad alta para capturar t0 cuanto antes
KernelEvents::TERMINATE => ['onKernelTerminate', -1024],
];
}
public function onKernelRequest(RequestEvent $event): void
{
if (!$event->isMainRequest()) {
return;
}
try {
$event->getRequest()->attributes->set(self::ATTR_START_TIME, microtime(true));
} catch (Throwable $e) {
// no-op: observabilidad no rompe flujo.
}
}
public function onKernelTerminate(TerminateEvent $event): void
{
try {
$request = $event->getRequest();
$t0 = $request->attributes->get(self::ATTR_START_TIME);
if (!is_float($t0)) {
return;
}
$dt = microtime(true) - $t0;
if ($dt < $this->thresholdSeconds) {
return;
}
$context = [
'route' => $request->attributes->get('_route'),
'method' => $request->getMethod(),
'path' => $request->getPathInfo(),
'user' => $this->resolveUser(),
'dt_ms' => (int) round($dt * 1000),
'status' => $event->getResponse()->getStatusCode(),
];
$this->observabilityLogger->safeLog('warning', '[OBS-PERF-TRAMITE] slow_request', $context);
} catch (Throwable $e) {
// swallow.
}
}
private function resolveUser(): ?string
{
try {
if ($this->tokenStorage === null) {
return null;
}
$token = $this->tokenStorage->getToken();
if ($token === null) {
return null;
}
return method_exists($token, 'getUserIdentifier') ? $token->getUserIdentifier() : (string) $token->getUser();
} catch (Throwable $e) {
return null;
}
}
}