hddev/laravel-request-logger

Structured HTTP request logging for Laravel: request lifecycle events, correlation IDs, sensitive data redaction, pluggable profiles and writers.

Maintainers

Package info

github.com/fredattack/laravel-request-logger

pkg:composer/hddev/laravel-request-logger

Transparency log

Statistics

Installs: 12

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v1.1.0 2026-07-27 10:22 UTC

This package is auto-updated.

Last update: 2026-07-27 10:31:44 UTC


README

Structured HTTP request logging for Laravel 12+. Logs the full request lifecycle (request.start, request.end, request.exception) as structured context, correlates everything with a request ID propagated through the Laravel Context and returned in a response header, and redacts sensitive data recursively.

Built for platforms where logs are queried, not read: every entry carries a machine-parsable event, a request_id, HTTP metadata, user identity and performance figures. On Azure Container Apps + Log Analytics, this makes any request traceable end to end with a single KQL query.

Why not spatie/laravel-http-logger?

Spatie's package logs the incoming request only. This package logs the lifecycle: start and end as two correlated events with status code, duration, memory peak, resolved route and authenticated user, plus a dedicated exception event. It borrows Spatie's best ideas: pluggable LogProfile / LogWriter classes and header sanitization.

Installation

composer require hddev/laravel-request-logger

Publish the config file if you need to customize beyond env variables:

php artisan vendor:publish --tag=request-logger-config

Usage

Register the middleware in bootstrap/app.php. Register it before auth middlewares so the whole request is timed; user identity is still captured on request.end because auth has run by then.

use HDDev\LaravelRequestLogger\Middleware\LogRequest;

->withMiddleware(function (Middleware $middleware): void {
    $middleware->prepend(LogRequest::class);
})

Or on a group only:

$middleware->appendToGroup('api', LogRequest::class);

What gets logged

[GET] api/demands                 request.start   query params, client info
[GET] demands.index (200)        request.end     status, route, user, duration, memory
[POST] demands.store (EXCEPTION) request.exception  exception class/message/file/line

Note on exceptions: with Laravel's exception handler active, throwables are rendered into a 500 response inside the routing pipeline, so they surface as a request.end error event (the handler's own report shares the same request_id through the Context). The request.exception event is a safety net for throwables that escape the handler (handler unbound, rethrow, some Octane edge cases).

Every response also receives an X-Request-ID header, and request_id is added to the Laravel Context, so it appears on every log line written during the request and is propagated to queued jobs.

Configuration

Everything ships with sensible defaults and is driven by env variables:

Variable Default Description
REQUEST_LOGGER_ENABLED true Master switch
REQUEST_LOGGER_CHANNEL null Dedicated log channel (falls back to default)
REQUEST_LOGGER_LOG_BODY false Log the sanitized request body on start
REQUEST_LOGGER_LOG_HEADERS false Log sanitized headers on start
REQUEST_LOGGER_REQUEST_ID_HEADER X-Request-ID Response header carrying the request ID
REQUEST_LOGGER_FAIL_SILENTLY true Never break a request on logging failure

Exclusions (paths with wildcards, route names, HTTP methods), sensitive keys/headers and the redaction placeholder are configured in config/request-logger.php.

Dedicated channel (recommended in production)

Keep request logs separate, queryable, and cheap to purge. Example config/logging.php channel writing JSON to stderr (Docker/Container Apps friendly):

'channels' => [
    'requests' => [
        'driver' => 'monolog',
        'handler' => Monolog\Handler\StreamHandler::class,
        'formatter' => Monolog\Formatter\JsonFormatter::class,
        'with' => ['stream' => 'php://stderr'],
    ],
],

Then set REQUEST_LOGGER_CHANNEL=requests.

Running Octane with the FrankenPHP server? Use OctaneSafeJsonFormatter instead of JsonFormatter. See Octane.

Extending

Three contracts, all swappable from config:

'log_profile' => App\Logging\MyLogProfile::class,          // decides WHAT to log
'log_writer' => App\Logging\MyLogWriter::class,            // decides HOW to log it
'request_id_generator' => App\Logging\MyIdGenerator::class, // decides the correlation ID

DefaultLogWriter exposes protected extension points (logger(), logLevel(), title(), performance()) so partial overrides stay small.

Full architecture walkthrough and extension use cases: docs/extending.md.

Octane

The middleware resolves its collaborators per request and stores the request ID in the Laravel Context, which Octane resets between requests. No state leaks across workers. Two consecutive requests always get distinct request IDs (covered by tests).

FrankenPHP: use OctaneSafeJsonFormatter on stderr

With the FrankenPHP server, Octane reads the worker's stderr and parses every line as a Caddy log entry. A line that decodes to JSON but has no msg key is replaced by ERROR unknown error. before it ever reaches the container log collector.

Monolog's JsonFormatter writes message, not msg. So the recommended stderr channel above silently loses every request log: the middleware runs, the X-Request-ID header is returned, but the structured lines never make it out. Nothing throws, and fail_silently has no effect, because the write itself succeeds.

Point the channel at OctaneSafeJsonFormatter, which wraps each record in the envelope Octane forwards verbatim:

'channels' => [
    'requests' => [
        'driver' => 'monolog',
        'handler' => Monolog\Handler\StreamHandler::class,
        'formatter' => HDDev\LaravelRequestLogger\Formatters\OctaneSafeJsonFormatter::class,
        'with' => ['stream' => 'php://stderr'],
    ],
],

The emitted line stays a single JSON object per record, with the original payload nested under msg:

{"msg":"{\"message\":\"[GET] api/demands\",\"context\":{\"event\":\"request.start\",\"request_id\":\"a25ca186\"},\"level_name\":\"INFO\",\"type\":\"raw\"}"}

Log collectors that index raw stderr lines still see valid JSON. To query the inner payload, parse the msg string as JSON (for example parse_json(msg) in KQL).

You only need this when writing to stderr under FrankenPHP. File channels, stdout-based drivers and the Swoole/RoadRunner servers are unaffected, and JsonFormatter remains the right choice there.

Testing

composer test            # pest
composer test-coverage   # pest --coverage --min=100
composer analyse         # phpstan level max
composer format          # pint

Changelog

See CHANGELOG.

License

The MIT License (MIT). See LICENSE.