crystoline/xpsr-log-monolog
Monolog as the XPSR application logger: logs to stderr, a file, daily files, or syslog, as plain lines or JSON, with a request ID on every line.
composer require crystoline/xpsr-log-monolog
Add the provider to app.registry in config/app.php, after FactoryRegistry (which binds the default logger), and the request ID middleware first in app.middleware:
'registry' => [
FactoryRegistry::class,
Crystoline\Xpsr\Log\Monolog\MonologServiceRegistry::class,
// ...
],
'middleware' => [
Crystoline\Xpsr\Log\Monolog\Http\RequestIdMiddleware::class,
CorsMiddleware::class,
// ...
],
Everything that logs through Psr\Log\LoggerInterface, including the exception handler's error reports, now goes through Monolog. Without the package, XPSR's default logger writes through PHP's error_log().
Configuration
Set environment variables, or override them in config/logging.php:
| Setting | Environment | Default | Values |
|---|---|---|---|
channel |
LOG_CHANNEL |
stderr |
stderr: standard error, which Docker, systemd, and PHP-FPM (with catch_workers_output) collect. file: one file. daily: one file per day, as app-2026-10-28.log, keeping days files. syslog: the system log. |
format |
LOG_FORMAT |
json when APP_ENV=production, otherwise line |
line: readable text. json: one JSON object per line, for log collectors. |
level |
LOG_LEVEL |
debug |
The lowest level written: debug, info, notice, warning, error, critical, alert, or emergency. |
path |
var/log/app.log |
The file for file and daily, relative to the project root. The directory is created if needed and must be writable by the web server and CLI users. |
|
days |
14 |
How many daily files to keep. | |
name |
app.name |
The channel name on each line. |
// config/logging.php
return ['channel' => 'daily', 'days' => 30];
Invalid values fail when the logger is first used, with the setting's name.
A line looks like this; context values that fill {placeholders} in the message are removed from the context:
[2026-10-28T09:15:02.481233+00:00] Shop.WARNING: Order 42 failed {"reason":"card declined"} {"request_id":"4f9c0b6e1d2a4c5e8f7a6b5c4d3e2f10"}Exceptions passed as ['exception' => $exception], as the exception handler does, are written with their stack trace.
Request IDs
RequestIdMiddleware gives each request an ID. It keeps an incoming X-Request-Id header of up to 128 letters, digits, ., _, or -, as set by load balancers and calling services, and generates a random 32-character ID otherwise. The ID:
- is added as
request_idto every log line written while the request is handled, including the error report for an exception; - is available to your code as the
request_idrequest attribute, and asCrystoline\Xpsr\Log\Monolog\RequestId::get(); - is returned in the
X-Request-Idresponse header, so a user reporting a problem can quote it. Error pages rendered for exceptions that escape every middleware do not carry the header; their log lines still carry the ID.
Console commands have no request ID.
Services
| Service | Provides |
|---|---|
Psr\Log\LoggerInterface |
The Monolog logger (also Monolog\Logger). Shared. |
Crystoline\Xpsr\Log\Monolog\RequestId |
The current request's ID. Shared. |
Crystoline\Xpsr\Log\Monolog\Http\RequestIdMiddleware |
Sets it for each request. |
To add Monolog handlers of your own, such as an error-tracking service, bind Psr\Log\LoggerInterface yourself in a provider listed after this one, or extend the logger in a provider's boot(): $this->container->get(Monolog\Logger::class)->pushHandler(...).
Requires PHP 8.4+.