roadrunner / psr-logger
PSR-3 logger that forwards application logs to the RoadRunner app-logger plugin over RPC
Fund package maintenance!
Requires
- php: >=8.2
- ext-json: *
- psr/log: ^2.0 || ^3.0
- roadrunner/app-logger: ^1.3
Requires (Dev)
- buggregator/trap: ^1.13
- llm/skills: ^1.14
- roxblnfk/unpoly: ^1.8
- spiral/code-style: ^2.3.1
- symfony/polyfill-php83: *
- testo/codecov: ^0.2.5
- testo/testo: ^0.10.55
- vimeo/psalm: ^7.0.1
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-11-03 19:12:03 UTC
README
PSR-3 logger that writes to RoadRunner's logs over RPC
A PSR-3 compatible logger implementation that integrates with RoadRunner's logging system via RPC calls. This package provides a bridge between PSR-3 logging standards and RoadRunner's centralized logging infrastructure.
Get Started
Installation
composer require roadrunner/psr-logger
Configuration
The logger sends messages over RPC to the RoadRunner AppLogger plugin, so RPC must be enabled in .rr.yaml. The app logs channel controls the output format and the minimum level:
rpc: listen: tcp://127.0.0.1:6001 logs: channels: app: level: debug
Basic Setup
use RoadRunner\Logger\Logger as AppLogger; use RoadRunner\PsrLogger\RpcLogger; // Initialize the RoadRunner app logger $rpc = \Spiral\Goridge\RPC\RPC::create('tcp://127.0.0.1:6001'); $appLogger = new AppLogger($rpc); // Create the PSR-3 compatible logger $logger = new RpcLogger($appLogger);
Logging Examples
// Basic logging with different levels $logger->emergency('System is unusable'); $logger->alert('Action must be taken immediately'); $logger->critical('Critical conditions'); $logger->error('Runtime errors'); $logger->warning('Warning conditions'); $logger->notice('Normal but significant condition'); $logger->info('Informational messages'); $logger->debug('Debug-level messages'); // Logging with context data $logger->info('User logged in', [ 'user_id' => 123, 'ip_address' => '192.168.1.1', 'user_agent' => 'Mozilla/5.0...' ]); // Using the generic log method $logger->log(\Psr\Log\LogLevel::ERROR, 'Something went wrong', [ 'exception' => $exception, // sent as class, message, code, file, line and trace ]);
RPC Logger vs STDERR Logger
The RPC logger provides several advantages over RoadRunner's built-in STDERR Logger:
- Log Level Control: RPC Logger controls the actual log level sent to RoadRunner server, ensuring proper level filtering and display in RoadRunner logs. Messages from STDERR Logger are processed by RoadRunner with
infolevel. - Context Support: RPC logger preserves structured context data (arrays, objects). STDERR Logger outputs only the message string, ignoring context.
Log Levels
Supported Log Level Types
The logger accepts log levels in multiple formats:
- String values:
'error','warning','info','debug' - PSR-3 constants:
\Psr\Log\LogLevel::ERROR,\Psr\Log\LogLevel::WARNING - Stringable objects: Any object implementing
\Stringableinterface - BackedEnum values: PHP 8.1+ backed enums with string values
- RoadRunner levels:
\RoadRunner\Logger\LogLevelenum cases fromroadrunner/app-logger
// String levels $logger->log('error', 'Error message'); // PSR-3 constants $logger->log(\Psr\Log\LogLevel::WARNING, 'Warning message'); // BackedEnum example enum LogLevel: string { case Error = 'error'; case Warning = 'warning'; case Info = 'info'; case Debug = 'debug'; } $logger->log(LogLevel::Error, 'Error via enum');
Log Level Mapping
The logger maps PSR-3 log levels to RoadRunner logging methods as follows:
| PSR-3 Level | RoadRunner Method |
|---|---|
| emergency | error |
| alert | error |
| critical | error |
| error | error |
| warning | warning |
| notice | info |
| info | info |
| debug | debug |
| log | log |
Context Handling
The logger supports structured logging with context arrays. Context data is processed by a context processor before being passed to the underlying RoadRunner logger.
The RoadRunner app-logger client drops a value it cannot encode, so before sending, invalid UTF-8 in the message and in context strings, keys and arrays at any depth is replaced with U+FFFD, and a NAN, INF or -INF value is sent as a string. With a custom processor, what JSON cannot hold inside an array is replaced as Monolog does instead of losing the attribute: a reference cycle or a resource becomes null, a non-finite float or a pure enum becomes 0. Objects returned by a custom processor at the top level of the context are sent as they are.
Context keys time, level, msg and logger are sent as context.time, context.level and so on: RoadRunner prints attributes next to its own fields with these names, and a duplicate key would override the real time, level or message in JSON readers.
Default Context Processor
By default, RpcLogger uses DefaultProcessor which can handle:
- Scalar values (string, int, float, bool)
- Arrays and nested arrays
- Resources (converted to resource type description)
- Objects via built-in object processors, tried in this order (the first one that accepts the object wins):
- DateTimeProcessor: Converts
\DateTimeInterfaceobjects to ISO 8601 format (ATOM) - ThrowableProcessor: Converts exceptions/errors to structured arrays with class, message, code, file, line, trace and the
previousexception. The trace is one string with a line per frame as ingetTraceAsString(), without arguments, cut to 8 frames by default and then ending with... N morefor the dropped ones: pass another number tonew ThrowableProcessor(...)and add it withwithObjectProcessors()to change it - JsonSerializableProcessor: Converts
\JsonSerializableobjects to the data theirjsonSerialize()returns, without the@classmarker. The data goes through the same processors, so a date in it is formatted as above rather than asjson_encode()would do it. An object that is also\Stringableis logged as this data, not as its string - StringableProcessor: Converts
\Stringableobjects to their string representation - FallbackProcessor: Converts any other objects to arrays with class name and public properties. Reference cycles are cut: a property pointing back to the object itself or to an object that contains it is left out, and an object reached again through an array is exported as its class name only
- DateTimeProcessor: Converts
Arrays and objects nested deeper than 10 levels, counting the context array itself as the first one, are replaced with the notice Over 10 levels deep, aborting normalization, and an array keeps its first 1000 items, followed by a ... key with the notice Over 1000 items (N total), aborting normalization. This keeps large object graphs and arrays that reference themselves from flooding the log. A \JsonSerializable object takes one level, and one more for the array if its jsonSerialize() returns one. Both limits can be changed:
$processor = DefaultProcessor::createDefault() ->withMaxDepth(5) ->withMaxItems(100);
$logger->info('Order processed', [ 'order_id' => 12345, 'customer' => [ 'id' => 67890, 'email' => 'customer@example.com' ], 'amount' => 99.99, 'processed_at' => new \DateTime(), 'metadata' => [ 'source' => 'web', 'campaign' => 'summer_sale' ] ]);
Flat Context
RoadRunner gets every context value as a string, so a nested array arrives as one JSON string: customer={"id":67890,"email":"customer@example.com"}. FlattenProcessor runs another processor, DefaultProcessor by default, and turns nested maps into dot-separated keys, so each value becomes an attribute of its own:
use RoadRunner\PsrLogger\Context\FlattenProcessor; $logger = new RpcLogger($appLogger, new FlattenProcessor()); // customer.id=67890 customer.email=customer@example.com tags=["new","vip"] $logger->info('Order processed', [ 'customer' => ['id' => 67890, 'email' => 'customer@example.com'], 'tags' => ['new', 'vip'], ]);
An array whose first key is an integer stays JSON: a list, including one with gaps after array_filter() or cut by the item limit. When two keys collide, as a.b and ['a' => ['b' => …]] do, the first one wins. To flatten a customized processor, pass it in: new FlattenProcessor($processor).
Extending DefaultProcessor
The recommended approach is to extend DefaultProcessor with custom object processors using the withObjectProcessors() method. This allows you to add your own object handling while keeping all the built-in processors.
use RoadRunner\PsrLogger\Context\DefaultProcessor; use RoadRunner\PsrLogger\Context\ObjectProcessor; /** * @implements ObjectProcessor<ActiveRecord> */ class EntityProcessor implements ObjectProcessor { public function canProcess(object $value): bool { return $value instanceof ActiveRecord; } public function process(object $value, callable $processor): mixed { return $processor($value->toArray()); } } // Extend default processor with your custom processor $processor = DefaultProcessor::createDefault() ->withObjectProcessors(new EntityProcessor()); $logger = new RpcLogger($appLogger, $processor); // Now entity objects will be automatically converted to arrays with essential data $logger->info('Order created', [ 'user' => $user, // User entity instance 'order' => $order, // Order entity instance ]);
Note
Custom processors added via withObjectProcessors() are placed before the built-in processors.
This means your custom processors take precedence and can override the default behavior for specific object types.
// Add multiple custom processors at once $processor = DefaultProcessor::createDefault() ->withObjectProcessors( new EntityProcessor(), new MoneyProcessor(), new CustomValueObjectProcessor(), ); $logger = new RpcLogger($appLogger, $processor);
You can also start with an empty processor and add only the processors you need:
use RoadRunner\PsrLogger\Context\ObjectProcessor\DateTimeProcessor; // Create empty processor and add only specific processors $processor = DefaultProcessor::create() ->withObjectProcessors( new DateTimeProcessor(), new EntityProcessor() ); $logger = new RpcLogger($appLogger, $processor);
Custom Context Processor
For advanced use cases, you can provide a completely custom context processor to the RpcLogger constructor:
use RoadRunner\Logger\Logger as AppLogger; use RoadRunner\PsrLogger\RpcLogger; // Using a completely custom processor $customProcessor = function (mixed $context): mixed { // Your custom processing logic return $context; }; $logger = new RpcLogger($appLogger, $customProcessor);