Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
22 / 22
100.00% covered (success)
100.00%
1 / 1
CRAP
100.00% covered (success)
100.00%
1 / 1
RequestTiming
100.00% covered (success)
100.00%
22 / 22
100.00% covered (success)
100.00%
1 / 1
2
100.00% covered (success)
100.00%
1 / 1
 handle
100.00% covered (success)
100.00%
22 / 22
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2
3namespace App\Http\Middleware;
4
5use Closure;
6use Illuminate\Database\Events\QueryExecuted;
7use Illuminate\Http\Request;
8use Illuminate\Support\Facades\DB;
9use Illuminate\Support\Facades\Log;
10use Illuminate\Support\Str;
11
12/**
13 * FIRE-1143 Deliverable 2: per-request timing log.
14 *
15 * Emits one structured log line per HTTP request to the 'request_timings'
16 * channel with: request_id, endpoint, wall_time_ms, db_query_count,
17 * db_time_ms, status. /health/timings (Deliverable 4) reads from the
18 * same file to compute p50/p95/p99 over the last 5 minutes.
19 *
20 * Coexists with App\Audit\QueryAuditor's DB::listen (FIRE-1143 finding §1).
21 * Each request gets a fresh closure, so the two listeners do not interfere.
22 *
23 * Registered globally (NOT inside the auth.token group) so it captures
24 * unauthenticated traffic too — 401s and Cloudflare scanner noise are
25 * useful to see in the timings.
26 */
27class RequestTiming
28{
29    public function handle(Request $request, Closure $next)
30    {
31        $requestId = (string) Str::uuid();
32        $request->attributes->set('request_id', $requestId);
33
34        $start = microtime(true);
35        $dbCount = 0;
36        $dbTotalMs = 0.0;
37        $status = 0;
38
39        DB::listen(function (QueryExecuted $q) use (&$dbCount, &$dbTotalMs): void {
40            $dbCount++;
41            $dbTotalMs += $q->time;
42        });
43
44        try {
45            $response = $next($request);
46            $status = method_exists($response, 'status') ? $response->status() : 0;
47
48            return $response;
49        } finally {
50            $wallMs = (int) round((microtime(true) - $start) * 1000);
51
52            // Path-only — never include query string, headers, or body to
53            // avoid leaking PII / tokens into the log file.
54            Log::channel('request_timings')->info('request', [
55                'request_id'     => $requestId,
56                'endpoint'       => $request->method().' '.$request->path(),
57                'wall_time_ms'   => $wallMs,
58                'db_query_count' => $dbCount,
59                'db_time_ms'     => (int) round($dbTotalMs),
60                'status'         => $status,
61            ]);
62        }
63    }
64}