Files
xamxam/docs/monolog-plan.md
Pontoporeia ae66c2baad 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
2026-05-20 12:28:31 +02:00

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 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