prettus/laravel-request-logger
Laravel middleware that logs every HTTP request and response to the database. Configurable logging per route, sensitive data redaction, custom log profiles, and Eloquent model integration make it a simple way to audit and debug API traffic in Laravel apps.
1. Install and pin deliberately
composer require prettus/laravel-request-logger
The package's last tagged release is from January 2018, so before committing, check what Composer actually resolves:
composer show prettus/laravel-request-logger
If your app is on Laravel 9+, the illuminate/* constraint may block installation. Decide up front whether you are (a) staying on an older/LTS app, (b) forking the package and bumping the constraint (the code surface is small — a middleware, a logger service, and a few handlers), or (c) skipping it and writing a 40-line middleware around Log::channel('requests'). Do not discover this halfway through a sprint.
2. Publish and read the config first
php artisan vendor:publish --provider="Prettus\RequestLogger\Providers\LaravelServiceProvider"
This drops config/request-logger.php — that file is the whole package. Read it top to bottom before writing any code. The four things that matter:
enabled — the master on/off switch (good place for env('REQUEST_LOGGER_ENABLED', ...)).handlers — where the log line goes (FileHandler, a daily-rotating handler, etc.), each with its own path/options.format — the line template, built from placeholders like {method}, {path}, {status}, {duration} (the config comments enumerate the full set).remove_sensitive_fields — keys that get masked in request/response bodies.3. Register the middleware
Laravel 5.5+ auto-discovers the service provider, but the middleware is not registered for you. Add it in app/Http/Kernel.php:
protected $middleware = [
// ...
\Prettus\RequestLogger\Middleware\RequestLoggerMiddleware::class,
];
or, preferably for a first experiment, only on a route group:
Route::middleware(\Prettus\RequestLogger\Middleware\RequestLoggerMiddleware::class)
->prefix('api')
->group(base_path('routes/api.php'));
4. First use case: prove the pipeline on one endpoint
Hit a single route, then:
tail -f storage/logs/request.log
You should see one line per request in your configured format. If nothing appears, check, in order: enabled in config, config cache (php artisan config:clear), middleware placement, and write permissions on the log path (php-fpm usually runs as www-data, not your shell user).
Where to look next: the middleware's handle() method and the Prettus\RequestLogger\Logger service it wraps. They are short and they define everything the package does — startLogging(), endLogging(), and addExtra(). Once you've read those, the package holds no surprises.
Environment-scoped logging. Keep the middleware in the global stack only in local/staging, and use route-level middleware in production so you're not writing a line for every health check, favicon, and asset request. A typical setup:
// Kernel.php — global only where you want full visibility
protected $middleware = [
// ...
app()->environment(['local', 'staging'])
? \Prettus\RequestLogger\Middleware\RequestLoggerMiddleware::class
: null,
];
Then attach it explicitly to the API groups you actually care about in production.
Attaching business context with addExtra(). The most valuable pattern with this package is enriching the log line with things the raw request doesn't know — authenticated user, tenant, correlation ID. Because the middleware logs at response time, data set after the middleware runs is available:
use Prettus\RequestLogger\Facades\RequestLogger;
// in a middleware that runs after auth, or in a controller
RequestLogger::addExtra('user_id', auth()->id());
RequestLogger::addExtra('tenant', tenant()->slug);
RequestLogger::addExtra('request_id', $request->header('X-Request-Id'));
Pair this with a format that prints them, and you get grep-able request logs: {date} {method} {path} {status} {duration} user={user_id} tenant={tenant} req={request_id}.
Correlation IDs across services. Generate or accept X-Request-Id in an early middleware, stash it on the request, feed it into addExtra(), and echo it back on the response. This turns the package from "access log" into something you can actually join against downstream service logs — the single highest-value upgrade you can make to a bare install.
Swapping handlers instead of editing the middleware. The handler is the extension point. Write your own class implementing the package's handler contract and register it in handlers:
'handlers' => [
[
'class' => \App\Logging\JsonRequestHandler::class,
// whatever options your handler reads from the array
],
],
That's how you emit JSON, ship to stdout for a container platform, or forward to an external collector without touching vendor code. For structured logs, a custom handler is far cleaner than trying to compose a giant format string and parse it later.
Route-level granularity for noisy endpoints. Webhooks, upload endpoints, and anything with a large body should get a group with its own configuration or simply be excluded. You can also flip enabled at runtime via the facade/config around specific code paths if you need to silence a known-chatty block.
Manual logging for non-standard flows. The facade works outside the middleware. In a console command, a queue job doing an HTTP callout, or a controller that needs to log a "before" snapshot, call RequestLogger::startLogging() and RequestLogger::endLogging() yourself. Treat this as an escape hatch, not the default — the middleware path is the well-trodden one.
Testing. Bind the facade and assert calls rather than asserting on file contents:
RequestLogger::shouldReceive('addExtra')->once()->with('user_id', 1);
For an integration-level check, point the handler path at a temp file in the test setup and assert the file contents after $this->get(...).
It's unmaintained — treat the dependency as debt. Last release 2018. Pin the version, and add a composer outdated check to your ritual. If you're on a modern Laravel, a maintained fork or an in-house middleware will be less work than fighting the constraint solver. The upside is that the surface is tiny and readable, so forking is genuinely viable.
Global middleware logs everything. The default "add it to $middleware" instruction is fine for a demo and painful in production: health checks, /favicon.ico, preflight OPTIONS, and asset requests all become log lines. Either scope the middleware or filter in your handler.
Body logging is a memory and disk hazard. getContent() on a request with a large upload or a fat JSON payload pulls it all into memory, and then into your log. If you enable request/response body capture: cap size, skip non-text content types, and never log multipart uploads. A 50 MB base64 image in a log file is a support ticket waiting to happen.
Streamed and binary responses break body capture. StreamedResponse and BinaryFileResponse won't give you useful content — getContent() returns false or throws on some versions. Your handler must guard for that, or you'll log garbage or fatal on file downloads. Same caution for responses that were already sent.
Sanitisation is key-matching, not a scanner. remove_sensitive_fields masks known keys (password, token, etc.). It will not catch secrets embedded in free-text fields, nor necessarily headers like Authorization and Cookie if you enable header logging. Review your format string specifically for header placeholders and decide explicitly whether those belong in a plaintext file. In regulated environments, assume you cannot log headers at all.
Middleware ordering determines both accuracy and completeness. {duration} is measured from when the middleware enters the stack — put it early for full request timing, but late enough (or use addExtra() downstream) to see the authenticated user. Picking one position and understanding the trade-off beats being surprised by user_id being empty.
No rotation in the plain file handler. If you use a single-path handler, you own logrotate (or switch to a daily handler). A forgotten request log is a classic way to fill a disk on a Saturday.
Config caching will bite you. php artisan config:cache bakes in enabled and the handler paths. Changing .env after a cache warm looks like the package "stopped working" — it didn't, you just need config:clear. Always clear config before debugging a "middleware doesn't fire" report.
Permissions, not logic, are the usual root cause. If log lines silently vanish, storage/logs is likely unwritable by the FPM user, or SELinux is blocking the path. Test from the CLI (which runs as your user) and from HTTP (which doesn't) — a CLI-only success is the tell.
Watch out for log channel interference. The package writes to its own file path by default; don't point it at the same path as a Monolog channel you've also configured with rotation, or you'll get interleaved, half-rotated files.
Prefer the handler contract over editing vendor code. Every real customisation request (JSON output, external sink, sampling, size truncation) belongs in a handler or a subclass. Keeping vendor untouched means composer update — however rare — stays safe, and your customisation is reviewable in your own repo.
Add a kill switch you can flip without a deploy. Since you'll inevitably ship this to production, make enabled => env('REQUEST_LOGGER_ENABLED', false) and turn it on per-environment. When the log volume spikes at 3 a.m., an env var change beats a hotfix.
How can I help you explore Laravel packages today?