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.