|
| 1 | +# PSR-3 logging support in MODX |
| 2 | + |
| 3 | +## What this does |
| 4 | + |
| 5 | +MODX currently can't use standard PHP logging tools like Monolog. The `modX::_log()` override bypasses xPDO's new PSR-3 support (commit `55fd34318a`) in several code paths, so a logger registered on xPDO never sees all of MODX's log output. |
| 6 | + |
| 7 | +This PR adds `setLogger(LoggerInterface $logger)` to modX, restructures `_log()` to dispatch to a PSR-3 logger when one is set, and adds 20 new tests (11 baseline, 9 PSR-3). |
| 8 | + |
| 9 | +```php |
| 10 | +use Monolog\Logger; |
| 11 | +use Monolog\Handler\StreamHandler; |
| 12 | + |
| 13 | +$logger = new Logger('modx'); |
| 14 | +$logger->pushHandler(new StreamHandler('/path/to/modx.log', Logger::DEBUG)); |
| 15 | +$logger->pushProcessor(new Monolog\Processor\IntrospectionProcessor()); |
| 16 | + |
| 17 | +$modx->setLogger($logger); |
| 18 | +$modx->setLogLevel(modX::LOG_LEVEL_DEBUG); // control what reaches the logger |
| 19 | +``` |
| 20 | + |
| 21 | +## Backward compatibility |
| 22 | + |
| 23 | +When no PSR-3 logger is registered (`$this->logger === null`, the default), all logging behavior is identical to the previous implementation. 11 baseline tests verify this. |
| 24 | + |
| 25 | +## Design decisions and alternatives |
| 26 | + |
| 27 | +Three questions came up during implementation that have meaningful trade-offs. The current implementation reflects one set of choices, but these could reasonably go a different way. |
| 28 | + |
| 29 | +### 1. Should the PSR-3 logger respect modX's `logLevel`, or receive everything? |
| 30 | + |
| 31 | +**Options considered:** |
| 32 | + |
| 33 | +- **A. Respect `logLevel` (chosen):** The PSR-3 logger only sees messages that pass the `logLevel` threshold. Users set `$modx->setLogLevel(modX::LOG_LEVEL_DEBUG)` when they want full visibility, and can filter further in their logger config (e.g. Monolog handler levels). |
| 34 | + |
| 35 | +- **B. Send everything to the logger:** The PSR-3 logger receives all messages regardless of `logLevel`. The logger's own filtering decides what to keep. |
| 36 | + |
| 37 | +**Why A was chosen:** Performance. Every `log()` call that isn't filtered out early triggers `debug_backtrace()` in the legacy path to resolve the calling file and line. The early `logLevel` check (an integer comparison) skips all that work for messages below threshold. With option B and the default `LOG_LEVEL_ERROR`, every DEBUG/INFO/WARN call would still build context and dispatch to the logger. The trade-off is one extra line of config (`setLogLevel`) when setting up a logger. |
| 38 | + |
| 39 | +**If you'd prefer B:** We remove the early return at the top of `_log()` for the PSR-3 path, or add a separate threshold property for the PSR-3 logger. Note that this also interacts with decision 2 below; if the backtrace is skipped for the PSR-3 path (as it is now), the performance cost of option B is much lower. |
| 40 | + |
| 41 | +### 2. Should `debug_backtrace()` be called for the PSR-3 path? |
| 42 | + |
| 43 | +**Options considered:** |
| 44 | + |
| 45 | +- **A. Skip backtrace for PSR-3 (chosen):** The `file` and `line` context fields are only populated if the caller explicitly passed them (e.g. `__FILE__`, `__LINE__`). When not provided, they're empty strings. Monolog users add `IntrospectionProcessor` for automatic caller info. |
| 46 | + |
| 47 | +- **B. Always resolve backtrace:** Every log call resolves the backtrace so that `file`/`line` are always present in the PSR-3 context, matching legacy behavior. |
| 48 | + |
| 49 | +**Why A was chosen:** `debug_backtrace()` is the most expensive operation in the log path. Monolog's `IntrospectionProcessor` does the same thing but only for messages that survive handler-level filtering — so a DEBUG message that gets discarded by the handler never triggers a backtrace. This gives users better control over the performance/detail trade-off. |
| 50 | + |
| 51 | +**If you'd prefer B:** Move the backtrace resolution to before the `_dispatchToPsrLogger()` call, so the PSR-3 context always has file/line. Simpler for users who don't want to configure Monolog processors, but costs a backtrace on every dispatched log call. |
| 52 | + |
| 53 | +### 3. Should the PSR-3 logger replace legacy output, or be additive? |
| 54 | + |
| 55 | +**Options considered:** |
| 56 | + |
| 57 | +- **A. Replace legacy output (chosen):** When a PSR-3 logger is set, legacy FILE/ECHO/HTML/ARRAY output is suppressed. Only modRegister logging is preserved alongside the PSR-3 logger. |
| 58 | + |
| 59 | +- **B. Additive (both):** Both the PSR-3 logger and legacy targets receive messages. The existing `error.log` continues alongside the custom logger. |
| 60 | + |
| 61 | +- **C. Configurable:** Add an option (e.g. `psr_logger_exclusive`) to let users choose at runtime. |
| 62 | + |
| 63 | +**Why A was chosen:** If you've set up Monolog, you're taking control of where logs go. Continuing to write to the legacy `error.log` is confusing and potentially wasteful. modRegister is preserved because it's a message queue mechanism, not traditional logging. |
| 64 | + |
| 65 | +**If you'd prefer B:** In the restructured `_log()`, remove the early return after the PSR-3 dispatch (lines 2989-2995 in the current code) and let execution fall through to the legacy path. The `$this->logger = null` / `try/finally` trick already exists for the legacy `parent::_log()` call and would prevent double-dispatch. |
| 66 | + |
| 67 | +**If you'd prefer C:** Add a config option check before the early return, e.g.: |
| 68 | +```php |
| 69 | +if ($hasPsrLogger && $this->getOption('psr_logger_exclusive', null, true)) { |
| 70 | + // skip legacy |
| 71 | + return; |
| 72 | +} |
| 73 | +// fall through to legacy path |
| 74 | +``` |
| 75 | + |
| 76 | +### Other notes |
| 77 | + |
| 78 | +- **xPDOLogger exclusion:** The bundled `xPDOLogger` (standalone xPDO's default logger) is explicitly excluded from modX's PSR-3 dispatch. Its `handleXpdo()` method calls `exit()` for FATAL, while modX uses `sendError('fatal')` which renders a proper error page. xPDOLogger is intended for standalone xPDO only. |
| 79 | + |
| 80 | +- **FATAL handling:** modX continues to use `sendError('fatal')` for fatal log messages, not `exit()`. The PSR-3 logger receives the FATAL message (mapped to `LogLevel::CRITICAL`) before `sendError()` is called. |
| 81 | + |
| 82 | +- **Backward compatibility:** When no PSR-3 logger is set (`$this->logger === null`, the default), all behavior is identical to the previous implementation. The baseline tests verify this. |
0 commit comments