roadrunner/psr-logger
PSR-3 logging adapter for RoadRunner. Route standard PSR-3 log messages through RoadRunner's logging facility so worker output stays centralized, structured, and consistent with the application server's configuration and transports. Works with any PSR-3-compatible framework or library.
This is a very small, single-purpose adapter: it gives you a PSR-3 LoggerInterface implementation that pushes log records through RoadRunner's logger RPC to the RoadRunner server, instead of writing to stdout/stderr directly from PHP. That's the entire value proposition — if you're writing to echo inside a worker you're interleaving output with the protocol stream; this routes it properly.
Minimal steps:
composer require roadrunner/psr-logger
use RoadRunner\Logger\Logger;
use Spiral\RoadRunner\Worker;
$logger = new Logger(Worker::create());
$logger->info('Worker booted', ['pid' => getmypid()]);
Where to look first: the package is only a handful of files under src/, so read them all before you wire anything up. Expect to find the main Logger class plus StdoutLogger / StderrLogger variants (or equivalents) and a small factory. Confirm three things in the source before designing around it: (1) whether PSR-3 {placeholder} interpolation is implemented, (2) how Throwable values in context['exception'] are serialized, and (3) whether the RPC handle is created lazily or eagerly.
First real use case: replace the ad-hoc fwrite(STDERR, ...) / echo calls scattered through a long-running RoadRunner worker with an injected PSR-3 logger, so log lines get the server-side formatter, timestamps, and level filtering from .rr.yaml rather than whatever your worker happened to print.
Inject the interface, not the class. Bind Psr\Log\LoggerInterface to the RoadRunner logger in your container and let application services type-hint the interface. This is the whole point of it being PSR-3 — you can swap in Monolog for tests and CLI commands and keep the production path on RoadRunner.
// bootstrap (Laravel-ish)
$this->app->singleton(LoggerInterface::class, fn () => new Logger(Worker::create()));
Framework bridge — use it as one channel, not the only one. In Laravel/Symfony you almost certainly still want Monolog for file logging, and you add this as an extra sink for worker-context messages. Practical shape: a Monolog Logger with a handler that forwards to the RoadRunner logger, or simply resolve LoggerInterface::class to RoadRunner inside worker entrypoints and to the framework logger elsewhere. Don't rip out Monolog; you lose channels, processors, and rotation.
Keep a request-scoped child logger. Since this is PSR-3, use $logger->withName()/context-merging in your own wrapper (PSR-3 has no withContext, so implement a tiny ContextLogger decorator) to attach request_id / trace_id once per job and let every downstream call inherit it. RoadRunner jobs are long-lived, so per-job context is cheap and enormously useful when you're grepping aggregated logs.
Log at boundaries, not in loops. Each call is an RPC round-trip to the server process. Log job start/end, retry attempts, and exceptions. Do not log per-row inside an import loop — batch a summary instead.
Use StdoutLogger/StderrLogger deliberately. If the variant exists for explicitly writing through RR's stdout/stderr channels, prefer StderrLogger for diagnostics: it survives even when the server-side logger pipeline is misconfigured, and it won't be confused with normal application log stream.
Structured context beats string interpolation. Pass scalars and small arrays in context; the RoadRunner server formatter handles the rest. Avoid pre-formatting exception messages into strings — pass ['exception' => $e] and let the adapter/server decide how much detail (stack trace, previous chain) to emit, gated by environment.
.rr.yaml's logs section (mode, level, encoding, output) controls what actually gets written. Your PHP $logger->debug() may still pay the full RPC cost and be dropped at the server. If debug logging is expensive in your code path, check the configured level before building the payload.{foo} in the message untouched, which silently breaks dashboards. If unsure, pre-interpolate.BufferLogger decorator wrapping it) rather than one RPC per line.context['exception'] is rendered with the full trace or dropped/cast to string. If it's truncated, log the trace separately or configure .rr.yaml output for it.fwrite(STDERR, "ping\n") in the worker and see if it reaches the server's output. That isolates "RPC/logger config is broken" from "my logger binding is wrong." Also run with rr serve -v/debug mode to see the server-side logger pipeline.PsrLogMessageProcessor-style pipeline and use this package only as the final transport handler.StreamHandler pointed at php://stderr gets you 90% of the benefit with zero new dependency, because RoadRunner already captures worker stderr. Reach for this package when you specifically want server-side level config, consistent formatting across all RoadRunner SDKs (Go, Python), and to keep the protocol stream clean.How can I help you explore Laravel packages today?