Skip to main content
Laravel, shipping fast.
Chapter 4 ยท The Request Pipeline

LogApiRequestsMiddleware: Request Auditing

Julian Beaujardin

An audit trail is essential for any API. You need to know who made what requests, what they sent, what happened, and how long it took. This isn't just for debugging, it's for compliance, security investigations, performance monitoring, and understanding user behavior. LogApiRequestsMiddleware captures everything non-destructively and asynchronously, so it never slows down your API.

What Gets Logged

For every request that passes through the middleware, this data is captured:

  • User ID: Which Bearer token made the request (extracted from the Bearer model)
  • HTTP Method: GET, POST, DELETE, etc.
  • Endpoint: The full URL path that was called
  • Request Body: Everything the client sent (all input parameters)
  • Response Body: Everything you sent back (response data)
  • HTTP Status Code: 200, 201, 400, 429, 500, etc.
  • Execution Time: How long the request took in milliseconds
  • Timestamp: When the request was made (implicit)
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;
    }
}

The key is the defer() function: it queues a callback to run after the response is sent to the client. This means logging never delays the response. The client gets their data instantly. The logging happens asynchronously in the background.

The SendToLogsJob is a queued job, not immediate logging:

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
        );
    }
}

This job is queued on its own 'license-logs' queue, separate from other background jobs. This isolation ensures that logging never blocks critical jobs like payment processing or cache warming.

Without async logging:

  • Request comes in: 0ms
  • Process request: 50ms
  • Create response: 10ms
  • Save to database logs: 30ms (I/O wait)
  • Send response: 20ms
  • Total time to response: 90ms worse

With async logging:

  • Request comes in: 0ms
  • Process request: 50ms
  • Create response: 10ms
  • Queue logging job: 2ms (just queueing, no I/O)
  • Send response immediately: client gets response in ~62ms
  • Background worker saves logs asynchronously: happens later

For a high-traffic API handling thousands of requests, this is a massive difference. You're talking about 18ms per request saved across thousands of requests = server capacity to handle 3x more traffic.

When your API returns 500 errors, the logs show what requests caused them, what data was sent, and what data was returned:

request: { "key": "lic_123", "domain": "example.com" }
response: { "error": "External API timeout", "code": "PROVIDER_ERROR" }
status: 503
execution_time: 30123ms

You immediately see the request timed out. Now you check the external API status and see it was down. Problem solved, you didn't break anything.

You notice one token made 10,000 requests in 5 minutes. The logs show exactly what they requested and help you determine whether it was malicious or misconfigured:

user_id: 42
method: GET
endpoint: /api/licenses
request: {}
status: 200
execution_time: 45ms

Thousands of successful license list requests. Looks like a bug in their client, it's looping. You can reach out and help them fix it.

Regulators often ask who accessed what data and when. Your logs have answers:

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}

The logs show execution_time for every request, so you can identify slow endpoints:

GET /api/licenses: 45ms average (good)
POST /api/license: 250ms average (slow!)
DELETE /api/license/{key}: 5000ms average (very slow!)

You can identify bottlenecks.

The logs capture everything the client sends in request and what you send back in response. This includes:

  • Query parameters
  • Form data
  • Request body
  • Response data

If this includes passwords, API keys, credit card numbers, or other sensitive data, those are logged too! For security, you should:

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!
        // ...
    );
});

Or use ShouldBeEncrypted on the job class (which this project does) to auto-encrypt logged data at rest.