attributes->set(self::STARTED_AT, microtime(true)); return $next($request); } public function terminate(Request $request, Response $response): void { try { $startedAt = $request->attributes->get(self::STARTED_AT); $token = $request->user()?->currentAccessToken(); ApiRequestLog::query()->create([ 'api_token_id' => $token?->getKey(), // Snapshotted so a revoked token's history is still readable // — which is precisely when someone reviews it. 'api_token_name' => $token?->getAttribute('name'), 'user_id' => $request->user()?->getKey(), 'method' => $request->getMethod(), 'route' => $this->routePattern($request), 'status' => $response->getStatusCode(), 'duration_ms' => is_float($startedAt) ? (int) round((microtime(true) - $startedAt) * 1000) : 0, 'created_at' => now(), ]); } catch (Throwable $e) { // Telemetry must never turn a successful API call into a failed // one. A full disk or a locked table is an operations problem, // not the caller's — and by this point the response has already // gone out regardless. Log::warning('Failed to record an API request: '.$e->getMessage()); } } /** * The matched route's *pattern*, never the resolved URI: a URI carries * the ids of the clients and files a caller touched, and that belongs * in the audit log rather than in volume telemetry. A request that * matched nothing is recorded under a placeholder, so 404 noise stays * countable without recording what was probed. */ private function routePattern(Request $request): string { $route = $request->route(); return $route === null ? '(unmatched)' : $route->uri(); } }