LogApiRequestsMiddleware: auditoria de peticiones
Un rastro de auditoría es esencial para cualquier API. Necesitas saber quién hizo qué peticiones, qué envió, qué pasó y cuánto tardó. No es sólo para depurar: es para cumplimiento normativo, investigaciones de seguridad, monitorización de rendimiento y entender el comportamiento de los usuarios. LogApiRequestsMiddleware lo captura todo de forma no destructiva y asíncrona, así que nunca ralentiza tu API.
Qué se registra
Para cada petición que pasa por el middleware se captura:
- Identificador de usuario: qué token Bearer hizo la petición
- Método HTTP: GET, POST, DELETE, etc.
- Endpoint: la ruta completa que se llamó
- Cuerpo de la petición: todo lo que envió el cliente
- Cuerpo de la respuesta: todo lo que devolviste
- Código de estado: 200, 201, 400, 429, 500, etc.
- Tiempo de ejecución: cuánto tardó, en milisegundos
- Marca de tiempo: cuándo se hizo
final readonly class LogApiRequestsMiddleware
{
public function handle(Request $request, Closure $next): Response
{
// Record start time
$startTime = microtime(true);
// Let request process
$response = $next($request);
// Use defer() to log AFTER response is sent
defer(callback: function () use ($request, $response, $startTime) {
// Identify the user (bearer token holder)
$token = $request->bearerToken();
$bearer = $token ? Bearer::where('token', $token)->first() : null;
$userId = $bearer->id ?? 0;
// Extract response data
$responseData = json_decode($response->getContent(), true) ?? [];
// Dispatch async logging job
SendToLogsJob::dispatch(
user_id: $userId,
method: $request->method(),
endpoint: $request->url(),
request: $request->all(),
response: $responseData,
status: $response->getStatusCode(),
execution_time: microtime(true) - $startTime
);
});
return $response;
}
}
La clave es la función defer(): encola un callback para ejecutarse después de que la respuesta se envíe al cliente. Eso significa que el registro nunca retrasa la respuesta. El cliente recibe sus datos al instante. El registro ocurre en segundo plano.
SendToLogsJob es un job en cola, no registro inmediato:
class SendToLogsJob implements ShouldQueue
{
public $tries = 3; // Retry up to 3 times if queuing fails
public $backoff = [30, 60, 120]; // Exponential backoff
public $onQueue = 'license-logs'; // Use dedicated queue
public function handle(): void
{
// Now the job actually logs to the LogFacade
LogFacade::create(
user_id: $this->user_id,
method: $this->method,
endpoint: $this->endpoint,
request: $this->request,
response: $this->response,
status: $this->status,
execution_time: $this->execution_time
);
}
}
Este job va en su propia cola 'license-logs', separada de otros trabajos en segundo plano. Ese aislamiento garantiza que el registro nunca bloquee jobs críticos como el procesamiento de pagos o el calentamiento de cachés.
Sin registro asíncrono:
- Llega la petición: 0 ms
- Se procesa: 50 ms
- Se crea la respuesta: 10 ms
- Se guarda en la base de datos: 30 ms (espera de E/S)
- Se envía la respuesta: 20 ms
- Tiempo total hasta la respuesta: 90 ms peor
Con registro asíncrono:
- Llega la petición: 0 ms
- Se procesa: 50 ms
- Se crea la respuesta: 10 ms
- Se encola el job de registro: 2 ms (sólo encolar, sin E/S)
- Se envía la respuesta de inmediato: el cliente la recibe en unos 62 ms
- Un worker guarda los registros en segundo plano: ocurre después
Para una API con mucho tráfico atendiendo miles de peticiones, es una diferencia enorme. Hablamos de 18 ms por petición ahorrados a través de miles de peticiones: capacidad para el triple de tráfico.
Cuando tu API devuelve errores 500, los registros muestran qué peticiones los causaron, qué datos se enviaron y qué se devolvió:
request: { "key": "lic_123", "domain": "example.com" }
response: { "error": "External API timeout", "code": "PROVIDER_ERROR" }
status: 503
execution_time: 30123ms
Ves de inmediato que la petición agotó su tiempo. Ahora compruebas el estado de la API externa y ves que estaba caída. Problema resuelto: no rompiste nada.
Notas que un token hizo diez mil peticiones en cinco minutos. Los registros muestran exactamente qué pidió y te ayudan a determinar si fue malicioso o una mala configuración:
user_id: 42
method: GET
endpoint: /api/licenses
request: {}
status: 200
execution_time: 45ms
Miles de listados de licencias correctos. Parece un fallo en su cliente: está en bucle. Puedes contactarles y ayudarles a arreglarlo.
Los reguladores suelen preguntar quién accedió a qué datos y cuándo. Tus registros tienen respuestas:
2026-02-22 14:35:22 | user_id: 15 | GET /api/licenses
2026-02-22 14:35:45 | user_id: 15 | POST /api/license
2026-02-22 14:36:01 | user_id: 15 | DELETE /api/license/{key}
Los registros guardan execution_time de cada petición, así que puedes identificar endpoints lentos y localizar cuellos de botella.
Los registros capturan todo lo que el cliente envía y lo que devuelves. Eso incluye parámetros de consulta, datos de formulario, cuerpo de la petición y datos de la respuesta. Si eso incluye contraseñas, claves de API, números de tarjeta u otros datos sensibles, ¡también se registran! Por seguridad deberías sanearlo:
defer(callback: function () use ($request, $response, $startTime) {
// ... existing code ...
// Sanitize sensitive fields
$requestData = $request->all();
unset($requestData['password'], $requestData['api_key'], $requestData['secret']);
SendToLogsJob::dispatch(
// ...
request: $requestData, // Sanitized!
// ...
);
});
O usar ShouldBeEncrypted en la clase del job (lo que hace este proyecto) para cifrar automáticamente los datos registrados en reposo.