always-open/laravel-request-logger
Always Open's Laravel Request Logger captures incoming HTTP requests in your Laravel application, giving you a record of traffic for debugging, monitoring, and audits. It aims to be a lightweight drop-in logging layer for existing projects, though it is an early-stage package with limited adoption s...
Version context (v4.0.0): this release is a Laravel 13 compatibility bump (FLUX-911). The major version number reflects a framework-constraint break rather than new features, so pick your version deliberately:
| Your app | Package version |
|---|---|
| Laravel 13 | ^4.0 |
| Laravel 12 and below | stay on ^3.1 |
composer require always-open/laravel-request-logger
If Composer refuses the install, composer why-not always-open/laravel-request-logger ^4.0 will tell you whether it's the illuminate/* constraint or your PHP version that's blocking it — check the package's composer.json in vendor/ for the exact php and illuminate/support floors before assuming your app is the problem.
First three steps:
php artisan vendor:publish --tag=request-logger-config
Read it top to bottom before touching code. The except/include path lists and the sensitive-field redaction list are the two settings that decide whether this package is an asset or a liability.php artisan vendor:publish --tag=request-logger-migrations
php artisan migrate
Route::middleware('request-logger')->group(function () {
Route::apiResource('orders', OrderController::class);
});
First use case: hit one endpoint, then inspect the resulting row via the package's model (typically RequestLogger::latest()->first() or your own query against the request_logs table). Confirm three things in that one row: the route/method were captured, the payload was captured (and redacted), and the timestamp/duration are sane. That single row tells you whether the middleware is placed correctly before you roll it out anywhere else.
Global vs. scoped logging. Global middleware gives you complete coverage but also logs health checks, Horizon, Telescope, and asset 404s within a week. The pragmatic default is a dedicated log channel plus a broad except list in config, and only escalate to route-group middleware on apps where volume matters. Whichever you choose, add it to bootstrap/app.php (Laravel 11+ style) rather than Kernel.php — v4 targets Laravel 13, where the old HTTP kernel is gone.
Redaction is the load-bearing config. Before enabling globally, open the config's sensitive-keys list and add anything your app-specific: password_confirmation, card_number, ssn, api_key, plus any tenant-specific fields. Filtering happens against the request payload, so nested keys matter — verify with a throwaway endpoint that posts a nested secret and confirm it lands as *** in the DB.
Log after the response, not during it. A DB insert on every request, inside the request lifecycle, adds latency to the hot path. Where the package supports it, defer the write to the middleware's terminate() method or push to a queued job. This is a one-line change that removes the insert from your p95. If the package doesn't offer it out of the box, wrap the model write in dispatch(fn () => ...)->afterResponse() or a small job.
Attach your own context. The generic row is far more useful with a correlation ID and an actor. Common approach: a small middleware ahead of the logger that calls Log::withContext() or writes to a request attribute the logger picks up, injecting user_id, tenant_id, and a X-Request-Id you also return in the response header. That header is what lets you go from a customer email to the exact logged request.
Prune from day one. Request logs grow linearly and then surprise you. Add the package model to a scheduled prune command ($schedule->command('model:prune')->daily() with a prunable() definition) or a plain ->daily() closure issuing RequestLogger::where('created_at', '<', now()->subDays(30))->delete(). Do this at install time, not after the table hits a million rows.
Testing. Disable the middleware in your feature tests ($this->withoutMiddleware(RequestLogger::class)) so unrelated assertions don't fill the table, and write one dedicated test that enables it and asserts the side effect you care about — redaction and status capture. That test is the canary for every future config change.
Consuming the logs. Expose a read path early: scopes by method/status/date, and a simple php artisan tinker or Nova/Filament resource. Logs nobody can query are dead weight.
Upgrading 3.x → 4.0 is not a config-free bump. The release only touches framework support, but the version constraint change means older Laravel installs will fail resolution. Do the upgrade on a branch, run composer update always-open/laravel-request-logger, then immediately diff your published config against the new default — vendor:publish --tag=request-logger-config --force will overwrite your redaction list, so diff rather than force. Then re-run your redaction test and php artisan config:clear (config caching will otherwise serve you the old values during local debugging).
Published files don't update themselves. This is the single most common post-upgrade trap with this class of package: migrations and config are yours once published. A new migration in the vendor directory in a future release will not run for you. After any upgrade, compare vendor/always-open/laravel-request-logger/config/ and database/migrations/ against what's in your app.
Middleware ordering matters for the response body. If you want status codes and response payloads, the logger must wrap the code that produces them — register it above SubstituteBindings but be aware that anything terminating first (like HandleCors or a caching layer) can short-circuit it, and you'll see successful requests logged as whatever the outer layer returns. If response capture is empty, the ordering is the first thing to check, not the config.
Don't serialize uploaded files or huge payloads. Reading $request->all() on a multipart request can hand you UploadedFile objects that don't serialize cleanly and can blow the column size. Set a max payload size in config, and skip logging bodies for multipart/form-data entirely. Relatedly, php://input is already consumed by the time Laravel has parsed a JSON body — use the request object, never re-read the stream, or you'll get empty bodies intermittently.
Column type and DB limits. Payloads routinely exceed TEXT on MySQL (65KB). Confirm the migration uses json or longText for payload columns, and watch max_allowed_packet in dev. On Postgres, jsonb is worth the swap for indexability if you plan to query inside payloads.
Volume and indexing. Add an index on created_at if the migration doesn't ship one — every dashboard query filters by it. If you're logging globally on a high-traffic app, measure the row growth rate for a day before deciding on retention; 1M rows/day is not unusual for an unexcluded global middleware.
Queue workers, Octane, and long-running processes. In Octane, middleware and any static state in the logger are held across requests — verify you're not accumulating context between requests (a user_id from request N leaking into request N+1 is the classic symptom). For queue workers, remember console jobs aren't HTTP requests, so nothing is logged there; don't expect request logs to explain background failures.
Extension points worth knowing. The model, the log formatter, and the config are all typically swappable — subclass the model to add tenant scoping or a custom connection, or bind your own formatter if you need a specific JSON shape for downstream ingestion. Before writing custom code, check whether the package exposes an event you can listen to instead, which survives updates far better than subclassing.
Debug checklist when nothing is logged: middleware actually registered (php artisan route:list -v shows it) → path not in the except list → config cache cleared → log connection/table exists → request didn't fail before the middleware ran. Work down that list in order; nine times out of ten it's step two.
How can I help you explore Laravel packages today?