WS-7 PR-2 commit 3. RFC §3.13. - app/Http/Middleware/BindRequestLogContext.php: tags every Laravel log line written during the request with request_id, organisation_id, user_id, and route name. Sets X-Request-Id on the response so the SPA can correlate to backend log lines via one click. - Client-supplied X-Request-Id is honoured only if it parses as a ULID via Str::isUlid. Junk input (empty, non-ULID) is rejected and a fresh ULID is generated server-side. - Registered as a global api-group middleware via the prepend list so it runs before authentication. Unauthenticated 4xx responses still carry the X-Request-Id header. - Test count: 1523 to 1532. Larastan clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
81 lines
2.2 KiB
PHP
81 lines
2.2 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Http\Middleware;
|
|
|
|
use App\Models\Event;
|
|
use App\Models\Organisation;
|
|
use Closure;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\Log;
|
|
use Illuminate\Support\Str;
|
|
use Symfony\Component\HttpFoundation\Response;
|
|
|
|
/**
|
|
* Structured-logging context binder (RFC-WS-7 §3.13). Tags every Laravel
|
|
* log line written during this request with request_id, organisation_id,
|
|
* user_id, and route name. Round-trips X-Request-Id with the response so
|
|
* the SPA can correlate to backend log lines via one click.
|
|
*/
|
|
final class BindRequestLogContext
|
|
{
|
|
public function handle(Request $request, Closure $next): Response
|
|
{
|
|
$requestId = $this->resolveRequestId($request);
|
|
$request->attributes->set('observability.request_id', $requestId);
|
|
|
|
Log::withContext(array_filter([
|
|
'request_id' => $requestId,
|
|
'organisation_id' => $this->resolveOrganisationId($request),
|
|
'user_id' => $request->user()?->getAuthIdentifier(),
|
|
'route' => $request->route()?->getName(),
|
|
], static fn ($v) => $v !== null && $v !== ''));
|
|
|
|
$response = $next($request);
|
|
|
|
$response->headers->set('X-Request-Id', $requestId);
|
|
|
|
return $response;
|
|
}
|
|
|
|
private function resolveRequestId(Request $request): string
|
|
{
|
|
$supplied = $request->header('X-Request-Id');
|
|
|
|
if (is_string($supplied) && Str::isUlid($supplied)) {
|
|
return $supplied;
|
|
}
|
|
|
|
return (string) Str::ulid();
|
|
}
|
|
|
|
private function resolveOrganisationId(Request $request): ?string
|
|
{
|
|
$portalEvent = $request->attributes->get('portal_event');
|
|
if ($portalEvent instanceof Event) {
|
|
return $portalEvent->organisation_id;
|
|
}
|
|
|
|
$route = $request->route();
|
|
if ($route === null) {
|
|
return null;
|
|
}
|
|
|
|
$org = $route->parameter('organisation');
|
|
if ($org instanceof Organisation) {
|
|
return $org->id;
|
|
}
|
|
if (is_string($org) && $org !== '') {
|
|
return $org;
|
|
}
|
|
|
|
$event = $route->parameter('event');
|
|
if ($event instanceof Event) {
|
|
return $event->organisation_id;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
}
|