Integrate Monolog: replace four logging systems with single PSR-3 factory

- 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
This commit is contained in:
Pontoporeia
2026-05-20 02:16:17 +02:00
parent a6e0aa5887
commit ae66c2baad
19 changed files with 662 additions and 433 deletions

141
docs/monolog-plan.md Normal file
View File

@@ -0,0 +1,141 @@
# 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