- Add monolog/monolog dependency (^3.10)
- Create app/Logger.php central factory with channels: app, admin, error, audit
- Each channel gets RotatingFileHandler (30-day retention) with pass-through LineFormatter
preserving existing JSON format contracts
- Rewrite AppLogger as thin facade delegating to Logger::get('app')
- Rewrite ErrorHandler::log() to delegate to Logger::get('error')
- Rewrite AdminLogger file output to delegate to Logger::get('admin'), keep DB writes
- Add Monolog file shadow to Audit via Logger::get('audit') (Option A per monolog-plan)
- Log level controlled by LOG_LEVEL env var (defaults: DEBUG in cli-server, WARNING otherwise)
- Graceful NullHandler fallback when log directory is not writable
- Update SystemController LOG_FILES: remove php_error, add app/admin/error/audit
- JSON app logs parsed to readable one-liners in the log viewer
- Remove nginx config tab (parametres + fragment + template + css)
- Friendly empty-state message when app log files don't exist yet (notYet)
- PHP tail fallback when exec() unavailable
- All 228 PHPUnit tests pass, no call sites changed
5.4 KiB
XAMXAM — Monolog Integration Plan
Goal
Replace the three separate logging systems (AppLogger, AdminLogger, ErrorHandler, Audit) with a single
Monolog-based logger, PSR-3 compliant, without changing any call sites in the first pass.
Prerequisites
composer require monolog/monolog
Step 1 — Understand the current landscape
Four logging systems exist. Map them before touching anything:
| Class | What it logs | Output | Call sites |
|---|---|---|---|
AppLogger |
App-level errors, warnings | File (JSON lines) | Scattered across controllers |
AdminLogger |
Admin actions, audit trail | File + DB | Admin controllers |
ErrorHandler |
PHP errors, exceptions | File (JSON lines) | Registered globally in boot |
Audit |
Data mutations (create/edit/delete) | DB table | DB layer, controllers |
Before writing any code, grep the codebase for every call site of each class and note the method signatures. The goal is to know exactly what the new unified interface must support before designing it.
Step 2 — Create a central Logger factory
Create app/Logger.php — a single factory/registry that holds named Monolog channel instances.
Do not replace any existing class yet. Just build the foundation.
// Channels to create:
// - 'app' → replaces AppLogger
// - 'admin' → replaces AdminLogger
// - 'error' → replaces ErrorHandler logging
// - 'audit' → replaces Audit (DB writes stay, but structured through Monolog)
Each channel gets:
- A
RotatingFileHandlerwriting tostorage/logs/{channel}.log, keeping 30 days - A
JsonFormatterso log lines stay JSON (preserving the existing format contract) - Log level set from an environment variable (
LOG_LEVEL, defaulting toWARNINGin production,DEBUGin dev)
The factory must be a simple static registry (Logger::get('app')) so existing call sites can be migrated
one file at a time without passing instances around.
Step 3 — Replace AppLogger
- Rewrite
AppLoggeras a thin wrapper that delegates toLogger::get('app') - Keep the existing public method signatures identical — no call sites change in this step
- Run the app, verify log output appears in
storage/logs/app.log - Delete the old file-writing implementation inside
AppLogger, keep the class as a facade for now
Step 4 — Replace ErrorHandler logging
- In
ErrorHandler, replace the internallog()method to delegate toLogger::get('error') - Monolog's
ErrorHandlerintegration can optionally replace the manualset_error_handler/set_exception_handlerregistration — evaluate whether to adopt that or keep the custom handler and just swap the write path - Verify that fatal errors and uncaught exceptions still produce log entries
Step 5 — Replace AdminLogger
This is the most complex because AdminLogger writes to both a file and the DB.
- File path → delegate to
Logger::get('admin')with aRotatingFileHandler - DB writes → keep as-is for now inside
AdminLogger, or add a custom MonologHandlerthat writes to the DB table. A custom handler is cleaner but optional in this pass. - Keep public method signatures identical
Step 6 — Replace Audit
Audit is DB-only (no file output). Two options:
- Option A (simple): Keep
Auditas-is, add a MonologLogger::get('audit')that shadows writes to a file for debuggability, call both fromAuditmethods - Option B (clean): Write a custom Monolog
AuditHandlerthat writes to the DB table, replaceAuditentirely
Option A is lower risk for this pass. Option B is the right long-term shape. Recommend Option A now, Option B as a follow-up.
Step 7 — Collapse the facades
Once all four classes delegate to Monolog internally, the facades (AppLogger, AdminLogger, etc.)
are just indirection. This step is optional in this pass but sets up the cleanup:
- Identify call sites that use
AppLogger::warning(...)style static calls - Decide whether to keep the facades permanently (low churn, acceptable) or migrate call sites
to
Logger::get('app')->warning(...)directly (cleaner, more churn) - A middle path: have the facades implement
Psr\Log\LoggerInterfaceexplicitly, which makes them swappable in tests
Step 8 — Add context standardisation
One of the main wins of Monolog over the current setup is structured context. Once the plumbing works, add processors to inject consistent fields into every log entry:
WebProcessor— adds URL, IP, HTTP method to every request log automatically- A custom processor for
request_id— generate a UUID per request inApp::boot()and attach it to all channels so log entries from one request can be correlated across channels
What NOT to do in this pass
- Do not change any call site outside the four logger classes
- Do not change log file paths or formats yet (other tooling may depend on them)
- Do not add Slack/email handlers yet — get the foundation right first
- Do not touch
Audit's DB schema
Definition of done
composer require monolog/monologis the onlycomposer.jsonchange- All four logging systems write through Monolog internally
- Existing log file locations and JSON format are preserved
- No call site outside the four logger classes has changed
AppLogger,AdminLogger,ErrorHandler,Auditstill exist and work as before from the outside- A single
LOG_LEVELenvironment variable controls verbosity across all channels