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.
What it is. roadrunner/psr-logger is a thin adapter — a PSR-3 LoggerInterface implementation that forwards log records over Goridge RPC to RoadRunner's logger plugin, so PHP worker output is written by the Go server's own log sink (its encoder, level, sampling, and worker metadata) rather than captured from stderr.
Architecture fit. For a Laravel-on-RoadRunner (Octane) deployment this fills a real gap. Today, worker logs reach the server sink only through stderr capture, which loses structure, level fidelity, and worker attribution. This adapter restores those. Critically, it is a transport, not a logging stack: no channels, no handlers, no formatters, no context processors, no rotation. It must sit underneath Monolog, never replace it. Any design that rebinds Psr\Log\LoggerInterface at the container level and routes the whole application through it is an architectural mistake — you would lose config/logging.php channels, per-channel stacks, Log::withContext()/shareContext(), processors, and Log::spy()-based testability.
Integration feasibility. Happy path is genuinely trivial: composer require, construct the logger with a RoadRunner worker instance in the worker bootstrap, emit. Effort is a few hours for a spike. The cost and risk live entirely at the edges — non-RoadRunner runtimes, shutdown paths, and level semantics.
Technical risk.
| Risk | Severity | Notes |
|---|---|---|
Protocol/version skew (PHP client ↔ Go server logger plugin) |
High | logger must be enabled and exposed in .rr.yaml; pin server and client versions together and re-verify on every RR upgrade |
| PSR-3 → RoadRunner level flattening | High | RR's logger historically exposes a reduced level set (debug/info/warn/error). notice, critical, alert, emergency collapse, silently breaking any alert routing keyed on Monolog levels |
| Non-RR runtime contexts | High | artisan, PHPUnit, local dev, queue workers on Supervisor, any non-Octane path have no server to talk to; every log call becomes a failed RPC |
| Shutdown/error-path logging | High | Fatal errors, OOM, and register_shutdown_function handlers run after the RPC channel may be closed — you lose exactly the records you most need. Needs an explicit stderr fallback in the fatal path |
| Context serialization | Medium | Context must be JSON-safe and size-bounded; closures, resources, and large Eloquent models either get dropped or inflate the frame |
| PSR-3 contract violation | Medium | PSR-3 expects log() not to throw on context problems; the transport can throw. Wrap and degrade |
| Duplicate emission | Low | If stderr capture stays enabled alongside RPC logging, entries double up in the sink |
Maturity read. 1 star and 0 dependents means you would be, functionally, the first external consumer. That is not a code-quality signal — the package is small and lives in the roadrunner-php org, which has vendor backing — but it does mean zero community bug reports, zero Stack Overflow corpus, zero third-party blog posts, and no other team's post-mortems to learn from. The composite score of ~19/100 is driven almost entirely by adoption, not by fit. Treat it as a vendorable utility with an adapter you own, not as a dependency you inherit support for.
Key questions to resolve before committing.
logger RPC plugin enabled and exposed in our pinned RoadRunner server config, and does the package's client constraint match that server version?critical/alert)?{placeholder} interpolation, and how does it serialize non-scalar context?log() swallow transport exceptions, or do we need our own guard?RR_MODE is unset or the RPC service is unreachable?roadrunner-laravel) already provide a logger bridge we should use instead of adding a second path?Stack fit. Laravel 10/11/12 on PHP 8.1+, Monolog 3, psr/log ^2|^3 (Monolog 3's constraint), running under Octane with the spiral/roadrunner-* worker stack. MIT license, no legal or commercial friction.
Recommended shape — Monolog handler, not PSR rebind.
config/logging.php ──► 'roadrunner' channel (custom driver)
│
▼
App\Logging\RoadRunnerHandler (extends AbstractProcessingHandler)
│ wraps
▼
RoadRunner\PsrLogger\Logger ──Goridge──► RR `logger` plugin ──► sink
Wrap the package's PSR logger inside a Monolog\Handler\AbstractProcessingHandler and register it as a custom channel driver. This preserves every Laravel logging affordance — channels, stacks, daily rotation for non-RR contexts, processors, Log::spy() — while using the package for what it is: a wire format. Reserve direct use of the PSR logger for the worker bootstrap window, before the Laravel container is available.
Fallback design is mandatory, not optional. Use a delegating logger that checks for a live RoadRunner worker at construction and falls back to a stderr/null handler otherwise. This keeps artisan, tests, and local dev working with the same configuration file and removes an entire class of "works in staging, silent in CI" bugs.
Migration path / sequencing.
logger plugin in .rr.yaml; check the package's composer constraints against your PHP and RR client versions; read the source for level mapping, placeholder interpolation, and exception behavior. This is the step that de-risks everything else.config/logging.php, feature-flagged via env. Unit tests for level mapping and context serialization; an integration test asserting the fallback path when no worker exists.Compatibility notes. Pin the package and the RoadRunner server in lockstep; a server upgrade that reshapes the logger RPC is the most likely breakage vector, and it will surface as silently dropped logs rather than an error. Keep stderr capture enabled during rollout, then disable it once the canary is clean to avoid duplicates.
Maintenance. The package is a few hundred lines and will rarely change. The durable maintenance surface is yours: the Monolog handler, the level-mapping contract, the .rr.yaml configuration, and the version pin between client and server. Budget ongoing ownership for the adapter, not for the dependency. Upgrades to RoadRunner are the recurring cost.
Support. MIT, community-only, GitHub issues in the roadrunner-php org, no SLA. RoadRunner's commercial support covers the server, not this package. With 1 star and 0 dependents there is no external knowledge base and no peer experience to draw on — your team's notes and the source code are the documentation. That is acceptable for a 300-line adapter and unacceptable for anything load-bearing without an internal owner named against it.
Scaling. Each record is a unidirectional Goridge frame write with no round trip, so latency per call is small but non-zero versus buffered stderr. At high log volume — verbose query logging, chatty queue workers — this becomes a measurable throughput tax and a source of frame-allocation churn. Enforce sampling and rate limiting in PHP (or lean on RR's log sampling config) before, not after, an incident. Cap context payload size explicitly; a single unbounded model serialization can dominate the frame budget.
Failure modes and mitigations.
register_shutdown_function and fatal-error handlers; verify with a deliberate OOM test.notice/critical/alert/emergency collapse into a smaller set, silently breaking alert routing. Mitigation: explicit mapping table, and encode the original level into the message context so downstream filters can recover it.Ramp-up. One to two engineer-days for someone fluent in Laravel logging; three to five for someone new to Goridge and the RoadRunner worker lifecycle, since debugging requires understanding where in the worker lifecycle the RPC channel is open. The conceptual load is higher than the code volume suggests — that asymmetry is the main hidden cost. Ship a one-page runbook: logs missing → check plugin config → check fallback binding → check shutdown path → check level mapping, in that order.
How can I help you explore Laravel packages today?