# 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 ```bash 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. ```php // 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 `RotatingFileHandler` writing to `storage/logs/{channel}.log`, keeping 30 days - A `JsonFormatter` so log lines stay JSON (preserving the existing format contract) - Log level set from an environment variable (`LOG_LEVEL`, defaulting to `WARNING` in production, `DEBUG` in 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 `AppLogger` as a thin wrapper that delegates to `Logger::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 internal `log()` method to delegate to `Logger::get('error')` - Monolog's `ErrorHandler` integration can optionally replace the manual `set_error_handler` / `set_exception_handler` registration — 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 a `RotatingFileHandler` - DB writes → keep as-is for now inside `AdminLogger`, or add a custom Monolog `Handler` that 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 `Audit` as-is, add a Monolog `Logger::get('audit')` that shadows writes to a file for debuggability, call both from `Audit` methods - **Option B (clean):** Write a custom Monolog `AuditHandler` that writes to the DB table, replace `Audit` entirely 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\LoggerInterface` explicitly, 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 in `App::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/monolog` is the only `composer.json` change - 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`, `Audit` still exist and work as before from the outside - A single `LOG_LEVEL` environment variable controls verbosity across all channels