spatie/laravel-http-logger
This Laravel middleware logs incoming HTTP requests to your default log, so you can still access the original request data if something goes wrong. An extra safety net for critical user submissions like lead-generating forms, with configurable log profile, writer, and channel.
Install the package and publish the config if you want to tune behavior:
composer require spatie/laravel-http-logger
php artisan vendor:publish \
--provider="Spatie\HttpLogger\HttpLoggerServiceProvider" \
--tag="config"
The first practical use case is a critical form submission—leads, orders, applications—where you want the original request body even if validation, a downstream API, or the database fails. Start by attaching the middleware to one route rather than globally:
use Spatie\HttpLogger\Middlewares\HttpLogger;
Route::post('/leads', StoreLeadController::class)
->middleware(HttpLogger::class);
For Laravel 11+ global registration:
->withMiddleware(function (Middleware $middleware) {
$middleware->append(\Spatie\HttpLogger\Middlewares\HttpLogger::class);
})
For Laravel 10 and below, add it to app/Http/Kernel.php under $middleware.
Look first at:
config/http-logger.php — enabled, log_profile, log_writer, log_channel, log_level, except, sanitize_headersSpatie\HttpLogger\Middlewares\HttpLogger — the middleware entry pointSpatie\HttpLogger\LogNonGetRequests — default profile, logs POST, PUT, PATCH, DELETE, and QUERYSpatie\HttpLogger\DefaultLogWriter — default message format and header/body handlingFor a quick sanity check, set HTTP_LOGGER_ENABLED=true, submit a test POST, and tail your configured log channel. Also remember: if config is cached, environment changes require php artisan config:clear before retesting.
Start route-scoped, then widen deliberately. Global middleware is convenient but will evaluate every request and can produce a lot of noise. Route or route-group middleware is usually the right default:
Route::middleware(\Spatie\HttpLogger\Middlewares\HttpLogger::class)
->group(function () {
Route::post('/leads', StoreLeadController::class);
Route::post('/orders', StoreOrderController::class);
});
Use a custom LogProfile when the default non-GET rule is too broad. The profile only decides whether a request should be logged:
namespace App\Support\HttpLogger;
use Illuminate\Http\Request;
use Spatie\HttpLogger\LogProfile;
class LogCriticalSubmissions implements LogProfile
{
public function shouldLogRequest(Request $request): bool
{
return $request->isMethod('post')
&& ($request->routeIs('leads.store') || $request->routeIs('orders.store'))
&& ! $request->is('health*');
}
}
// config/http-logger.php
'log_profile' => App\Support\HttpLogger\LogCriticalSubmissions::class,
Use a custom LogWriter when you want structured, queryable logs. The default writer produces a human-readable line. For production triage, a JSON context line with route, user, IP, and request ID is much easier to search:
namespace App\Support\HttpLogger;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Spatie\HttpLogger\LogWriter;
class StructuredHttpLogWriter implements LogWriter
{
public function logRequest(Request $request): void
{
$sanitizedHeaders = array_map(
'strtolower',
config('http-logger.sanitize_headers', [])
);
$headers = collect($request->headers->all())
->map(fn (array $values) => $values[0] ?? null)
->map(fn ($value, string $name) => in_array($name, $sanitizedHeaders, true)
? '****'
: $value)
->all();
Log::channel(config('http-logger.log_channel'))
->log(config('http-logger.log_level', 'info'), 'http.request', [
'method' => strtoupper($request->method()),
'uri' => $request->getPathInfo(),
'route' => $request->route()?->getName(),
'ip' => $request->ip(),
'user_id' => $request->user()?->id,
'request_id' => $request->header('X-Request-Id'),
'body' => $request->except(config('http-logger.except', [])),
'headers' => $headers,
]);
}
}
'log_writer' => App\Support\HttpLogger\StructuredHttpLogWriter::class,
Give HTTP logs their own channel and retention policy. Do not let critical request logs compete with or duplicate into your application’s default stack:
// config/logging.php
'http-logger' => [
'driver' => 'daily',
'path' => storage_path('logs/http-logger.log'),
'level' => 'info',
'days' => 14,
],
// config/http-logger.php
'log_channel' => env('HTTP_LOGGER_CHANNEL', 'http-logger'),
HTTP_LOGGER_CHANNEL=http-logger
Sanitize both body and headers. except only filters request body fields; it does not redact headers. Add sensitive headers explicitly:
'excerpt' => [
'password',
'password_confirmation',
'token',
'card_number',
],
'sanitize_headers' => [
'Authorization',
'Cookie',
'Set-Cookie',
'X-Api-Key',
],
Use a custom writer if you need an allowlist rather than a denylist for body fields—especially for forms with nested payloads.
Disable logging in tests and local noise-heavy environments. Set HTTP_LOGGER_ENABLED=false in phpunit.xml or the test environment, and keep it enabled only where the safety net is valuable. For assertions, point http-logger.log_writer at a small fake writer in the test and assert what it received.
HTTP_LOGGER_ENABLED is read through config. If you change it in .env and use config:cache, run php artisan config:clear and recache. A stale config is the most common “why is it still logging?” issue.QUERY method support requires a recent version. It was added in 1.12.0, which also dropped PHP < 8.3 and Laravel < 12. Pin the package version if you rely on QUERY requests.except is body-only and top-level by default in the published config. Nested payloads such as user.password or payment.card can still leak. Verify with a real payload and use a custom writer with explicit allowlisting for high-risk forms.sanitize_headers must cover Authorization, Cookie, Set-Cookie, and API-key headers. The default writer includes headers in recent versions, so an unsanitized token will end up in logs.stack channel can duplicate logs. If the stack contains multiple handlers, the same request may be written multiple times. Use a single dedicated channel for HTTP request logs.try/catch so logging failures do not break submissions.How can I help you explore Laravel packages today?