mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-06-25 08:09:18 +02:00
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:
File diff suppressed because one or more lines are too long
4
TODO.md
4
TODO.md
@@ -225,8 +225,12 @@
|
|||||||
- [x] Step 4 — Update templates (data-queue-type on all inputs, data-existing-files in edit)
|
- [x] Step 4 — Update templates (data-queue-type on all inputs, data-existing-files in edit)
|
||||||
- [x] Step 5 — Update upload-progress.js (new collectFileNames, pending-uploads guard)
|
- [x] Step 5 — Update upload-progress.js (new collectFileNames, pending-uploads guard)
|
||||||
- [ ] Step 6 — QA / integration testing
|
- [ ] Step 6 — QA / integration testing
|
||||||
|
- [x] Logs accessible via Paramètres: app, admin, error, audit tabs (JSON parsed to readable lines), nginx tabs kept
|
||||||
|
- [x] Remove nginx config tab and PHP-FPM error log tab from UI
|
||||||
- [ ] Step 7 — Cleanup: remove transition flags, remove INPUT_ID_TO_TYPE
|
- [ ] Step 7 — Cleanup: remove transition flags, remove INPUT_ID_TO_TYPE
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# CSP & Deploy Fixes (May 2026)
|
# CSP & Deploy Fixes (May 2026)
|
||||||
|
|
||||||
- [x] Track vendor JS files in jj (they were moved to vendor/ but never `jj file track`ed)
|
- [x] Track vendor JS files in jj (they were moved to vendor/ but never `jj file track`ed)
|
||||||
|
|||||||
@@ -48,11 +48,9 @@ $diskPct = $diskInfo['pct'];
|
|||||||
$diskColor = SystemController::diskColor($diskPct);
|
$diskColor = SystemController::diskColor($diskPct);
|
||||||
|
|
||||||
// ── Logs section ──────────────────────────────────────────────────────────────
|
// ── Logs section ──────────────────────────────────────────────────────────────
|
||||||
$activeTab = $_GET['tab'] ?? 'nginx_access';
|
$activeTab = $_GET['tab'] ?? 'app';
|
||||||
if ($activeTab === 'status') {
|
if ($activeTab === 'status' || !array_key_exists($activeTab, SystemController::LOG_FILES)) {
|
||||||
$activeTab = 'nginx_access';
|
$activeTab = 'app';
|
||||||
} elseif ($activeTab !== 'nginx_config' && !array_key_exists($activeTab, SystemController::LOG_FILES)) {
|
|
||||||
$activeTab = 'nginx_access';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$selectedN = isset($_GET['n']) ? (int) $_GET['n'] : 100;
|
$selectedN = isset($_GET['n']) ? (int) $_GET['n'] : 100;
|
||||||
@@ -60,27 +58,12 @@ if (!in_array($selectedN, SystemController::ALLOWED_LINES, true)) {
|
|||||||
$selectedN = 100;
|
$selectedN = 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
$logLines = null;
|
$logData = $_controller->getLogData($activeTab, $selectedN);
|
||||||
$logError = null;
|
$logLines = $logData['lines'];
|
||||||
$logFileMeta = null;
|
$logError = $logData['error'];
|
||||||
|
$logFileMeta = $logData['meta'];
|
||||||
$nginxConfigLines = null;
|
$logIsJson = $logData['isJson'] ?? false;
|
||||||
$nginxConfigSource = null;
|
$notYet = $logData['notYet'] ?? false;
|
||||||
$nginxConfigError = null;
|
|
||||||
$nginxConfigMeta = null;
|
|
||||||
|
|
||||||
if ($activeTab === 'nginx_config') {
|
|
||||||
$nginxData = $_controller->getNginxConfigData();
|
|
||||||
$nginxConfigLines = $nginxData['lines'];
|
|
||||||
$nginxConfigSource = $nginxData['source'];
|
|
||||||
$nginxConfigMeta = $nginxData['meta'];
|
|
||||||
$nginxConfigError = $nginxData['error'];
|
|
||||||
} else {
|
|
||||||
$logData = $_controller->getLogData($activeTab, $selectedN);
|
|
||||||
$logLines = $logData['lines'];
|
|
||||||
$logError = $logData['error'];
|
|
||||||
$logFileMeta = $logData['meta'];
|
|
||||||
}
|
|
||||||
|
|
||||||
$collapsed = $_COOKIE['sys_collapsed'] ?? null;
|
$collapsed = $_COOKIE['sys_collapsed'] ?? null;
|
||||||
$statusInitiallyCollapsed = $collapsed === '1';
|
$statusInitiallyCollapsed = $collapsed === '1';
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
/**
|
/**
|
||||||
* system-fragment.php — returns only the tab-panel HTML for the admin system page.
|
* system-fragment.php — returns only the tab-panel HTML for the admin system page.
|
||||||
*
|
*
|
||||||
* Called by fetch() from system.php JS when switching tabs or changing line count.
|
* Called by fetch() from parametres.php JS when switching tabs or changing line count.
|
||||||
* With JS disabled the user never hits this URL directly; the tab <a> hrefs still
|
* With JS disabled the user never hits this URL directly; the tab <a> hrefs still
|
||||||
* point at system.php?tab=… so navigation degrades gracefully.
|
* point at parametres.php?tab=… so navigation degrades gracefully.
|
||||||
*
|
*
|
||||||
* Response: text/html fragment (no <html>/<head>/<body> wrapper).
|
* Response: text/html fragment (no <html>/<head>/<body> wrapper).
|
||||||
* On any auth failure or bad request: 403 / 400 with a plain-text body.
|
* On any auth failure or bad request: 403 / 400 with a plain-text body.
|
||||||
@@ -23,9 +23,9 @@ if (!AdminAuth::isAuthenticated()) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── Validate inputs ────────────────────────────────────────────────────────
|
// ── Validate inputs ────────────────────────────────────────────────────────
|
||||||
$activeTab = $_GET['tab'] ?? 'nginx_access';
|
$activeTab = $_GET['tab'] ?? 'app';
|
||||||
if ($activeTab !== 'nginx_config' && !array_key_exists($activeTab, SystemController::LOG_FILES)) {
|
if (!array_key_exists($activeTab, SystemController::LOG_FILES)) {
|
||||||
$activeTab = 'nginx_access';
|
$activeTab = 'app';
|
||||||
}
|
}
|
||||||
|
|
||||||
$selectedN = isset($_GET['n']) ? (int) $_GET['n'] : 100;
|
$selectedN = isset($_GET['n']) ? (int) $_GET['n'] : 100;
|
||||||
@@ -42,19 +42,11 @@ $_cache = new SystemCache($_db->getPDO());
|
|||||||
$_controller = new SystemController($_db, $_cache);
|
$_controller = new SystemController($_db, $_cache);
|
||||||
|
|
||||||
// ── Render ─────────────────────────────────────────────────────────────────
|
// ── Render ─────────────────────────────────────────────────────────────────
|
||||||
if ($activeTab === 'nginx_config') {
|
$logData = $_controller->getLogData($activeTab, $selectedN);
|
||||||
$nginxData = $_controller->getNginxConfigData();
|
$logLines = $logData['lines'];
|
||||||
$nginxConfigLines = $nginxData['lines'];
|
$logError = $logData['error'];
|
||||||
$nginxConfigSource = $nginxData['source'];
|
$logFileMeta = $logData['meta'];
|
||||||
$nginxConfigMeta = $nginxData['meta'];
|
$logIsJson = $logData['isJson'] ?? false;
|
||||||
$nginxConfigError = $nginxData['error'];
|
$notYet = $logData['notYet'] ?? false;
|
||||||
|
|
||||||
include APP_ROOT . '/templates/admin/partials/system-nginx-config-panel.php';
|
include APP_ROOT . '/templates/admin/partials/system-log-panel.php';
|
||||||
} else {
|
|
||||||
$logData = $_controller->getLogData($activeTab, $selectedN);
|
|
||||||
$logLines = $logData['lines'];
|
|
||||||
$logError = $logData['error'];
|
|
||||||
$logFileMeta = $logData['meta'];
|
|
||||||
|
|
||||||
include APP_ROOT . '/templates/admin/partials/system-log-panel.php';
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -352,29 +352,4 @@
|
|||||||
border: 1px solid var(--success-muted-border);
|
border: 1px solid var(--success-muted-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Nginx config viewer ───────────────────────────────────────────────── */
|
|
||||||
.nginx-source-badge {
|
|
||||||
display: inline-block;
|
|
||||||
font-size: var(--step--2);
|
|
||||||
font-family: ui-monospace, monospace;
|
|
||||||
padding: var(--space-3xs) var(--space-2xs);
|
|
||||||
border-radius: var(--radius);
|
|
||||||
margin-left: var(--space-2xs);
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
.nginx-source-badge--live {
|
|
||||||
background: var(--success-muted-bg);
|
|
||||||
color: var(--success);
|
|
||||||
border: 1px solid var(--success-muted-border);
|
|
||||||
}
|
|
||||||
.nginx-source-badge--local {
|
|
||||||
background: var(--warning-muted-bg);
|
|
||||||
color: var(--warning);
|
|
||||||
border: 1px solid var(--warning-muted-border);
|
|
||||||
}
|
|
||||||
/* Nginx syntax highlight layers inside .log-output */
|
|
||||||
.nginx-comment { color: var(--sys-syntax-comment); font-style: italic; }
|
|
||||||
.nginx-directive { color: var(--sys-syntax-directive); }
|
|
||||||
.nginx-block { color: var(--sys-syntax-block); font-weight: 600; }
|
|
||||||
.nginx-value { color: var(--sys-syntax-value); }
|
|
||||||
.nginx-location { color: var(--sys-syntax-location); }
|
|
||||||
|
|||||||
@@ -14,21 +14,10 @@
|
|||||||
*/
|
*/
|
||||||
class AdminLogger
|
class AdminLogger
|
||||||
{
|
{
|
||||||
private string $logFile;
|
|
||||||
private ?Database $db;
|
private ?Database $db;
|
||||||
|
|
||||||
public function __construct(?Database $db = null)
|
public function __construct(?Database $db = null)
|
||||||
{
|
{
|
||||||
if (php_sapi_name() === 'cli-server') {
|
|
||||||
$dir = defined('STORAGE_ROOT') ? STORAGE_ROOT . '/logs' : __DIR__ . '/../storage/logs';
|
|
||||||
if (!is_dir($dir)) {
|
|
||||||
mkdir($dir, 0755, true);
|
|
||||||
}
|
|
||||||
$this->logFile = $dir . '/admin.log';
|
|
||||||
} else {
|
|
||||||
$this->logFile = '/var/log/xamxam.log';
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->db = $db;
|
$this->db = $db;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -256,10 +245,10 @@ class AdminLogger
|
|||||||
$entry['context'] = $context;
|
$entry['context'] = $context;
|
||||||
}
|
}
|
||||||
|
|
||||||
$line = json_encode($entry, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) . "\n";
|
$line = json_encode($entry, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
|
||||||
if (is_writable($this->logFile) || (!file_exists($this->logFile) && is_writable(dirname($this->logFile)))) {
|
|
||||||
error_log($line, 3, $this->logFile);
|
// File output — delegates to Monolog 'admin' channel
|
||||||
}
|
Logger::get('admin')->info($line);
|
||||||
|
|
||||||
if ($this->db !== null) {
|
if ($this->db !== null) {
|
||||||
$this->insertDb($resource, $action, $status, $context);
|
$this->insertDb($resource, $action, $status, $context);
|
||||||
|
|||||||
@@ -3,9 +3,8 @@
|
|||||||
/**
|
/**
|
||||||
* Structured application logger for form submissions.
|
* Structured application logger for form submissions.
|
||||||
*
|
*
|
||||||
* Writes JSON-lines to a log file in storage/logs/.
|
* Thin facade over Monolog channel 'app'.
|
||||||
* Each entry contains: timestamp, source (admin|partage), action,
|
* Delegates all file I/O — keeps existing public API unchanged.
|
||||||
* status (success|error), context (IP, UA, thesis ID, error message, etc.).
|
|
||||||
*/
|
*/
|
||||||
class AppLogger
|
class AppLogger
|
||||||
{
|
{
|
||||||
@@ -16,10 +15,7 @@ class AppLogger
|
|||||||
{
|
{
|
||||||
$this->logDir = $logDir ?? (defined('STORAGE_ROOT') ? STORAGE_ROOT . '/logs' : __DIR__ . '/../storage/logs');
|
$this->logDir = $logDir ?? (defined('STORAGE_ROOT') ? STORAGE_ROOT . '/logs' : __DIR__ . '/../storage/logs');
|
||||||
|
|
||||||
if (!is_dir($this->logDir)) {
|
// Keep for backward compat — actual file I/O is now handled by Monolog via Logger::get('app')
|
||||||
mkdir($this->logDir, 0755, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->logFile = $this->logDir . '/form-submissions.log';
|
$this->logFile = $this->logDir . '/form-submissions.log';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,7 +84,7 @@ class AppLogger
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Write a structured log line.
|
* Write a structured log line — delegates to Monolog 'app' channel.
|
||||||
*/
|
*/
|
||||||
private function write(array $entry): void
|
private function write(array $entry): void
|
||||||
{
|
{
|
||||||
@@ -96,7 +92,8 @@ class AppLogger
|
|||||||
$entry['ip'] = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
|
$entry['ip'] = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
|
||||||
$entry['user_agent'] = $_SERVER['HTTP_USER_AGENT'] ?? '';
|
$entry['user_agent'] = $_SERVER['HTTP_USER_AGENT'] ?? '';
|
||||||
|
|
||||||
$line = json_encode($entry, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) . "\n";
|
$line = json_encode($entry, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
|
||||||
error_log($line, 3, $this->logFile);
|
|
||||||
|
Logger::get('app')->info($line);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ class Audit
|
|||||||
?array $oldData = null,
|
?array $oldData = null,
|
||||||
?array $newData = null
|
?array $newData = null
|
||||||
): void {
|
): void {
|
||||||
|
// DB write is the primary path — best-effort, never crash.
|
||||||
try {
|
try {
|
||||||
$stmt = $db->getConnection()->prepare(
|
$stmt = $db->getConnection()->prepare(
|
||||||
'INSERT INTO audit_log (actor, action, table_name, record_id, old_data, new_data)
|
'INSERT INTO audit_log (actor, action, table_name, record_id, old_data, new_data)
|
||||||
@@ -49,7 +50,33 @@ class Audit
|
|||||||
]);
|
]);
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
// Audit logging is best-effort — never crash the app over it.
|
// Audit logging is best-effort — never crash the app over it.
|
||||||
error_log('[Audit] write failed: ' . $e->getMessage());
|
error_log('[Audit] DB write failed: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
// File shadow — structured JSON-line log for debuggability
|
||||||
|
// (Option A from monolog-plan: keep Audit DB logic as-is, add file trace)
|
||||||
|
try {
|
||||||
|
$entry = [
|
||||||
|
'timestamp' => date('c'),
|
||||||
|
'ip' => $_SERVER['REMOTE_ADDR'] ?? 'cli',
|
||||||
|
'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? '',
|
||||||
|
'actor' => $actor,
|
||||||
|
'action' => $action,
|
||||||
|
'table' => $tableName,
|
||||||
|
'record_id' => $recordId,
|
||||||
|
];
|
||||||
|
if ($oldData !== null) {
|
||||||
|
$entry['old_data'] = $oldData;
|
||||||
|
}
|
||||||
|
if ($newData !== null) {
|
||||||
|
$entry['new_data'] = $newData;
|
||||||
|
}
|
||||||
|
|
||||||
|
$line = json_encode($entry, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
|
||||||
|
Logger::get('audit')->info($line);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
// File shadow is also best-effort
|
||||||
|
error_log('[Audit] file shadow write failed: ' . $e->getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,9 +11,8 @@
|
|||||||
* maintenance mode) with SystemCache TTL caching
|
* maintenance mode) with SystemCache TTL caching
|
||||||
* - PHP environment info (1-hour TTL)
|
* - PHP environment info (1-hour TTL)
|
||||||
* - Disk usage info (5-minute TTL)
|
* - Disk usage info (5-minute TTL)
|
||||||
* - Log file reading (tail, meta)
|
* - Log file reading (tail, meta, JSON-line parsing)
|
||||||
* - Nginx config file reading
|
* - Log line classifiers used by both system.php and system-fragment.php
|
||||||
* - Log/nginx line classifiers used by both system.php and system-fragment.php
|
|
||||||
*
|
*
|
||||||
* Both system.php (full page) and system-fragment.php (AJAX panel) delegate
|
* Both system.php (full page) and system-fragment.php (AJAX panel) delegate
|
||||||
* here so helpers are never duplicated.
|
* here so helpers are never duplicated.
|
||||||
@@ -23,17 +22,39 @@ class SystemController
|
|||||||
// ── Constants ─────────────────────────────────────────────────────────────
|
// ── Constants ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
public const LOG_FILES = [
|
public const LOG_FILES = [
|
||||||
'nginx_access' => ['label' => 'nginx — accès', 'path' => '/var/log/nginx/xamxam_access.log'],
|
'app' => ['label' => 'App — soumissions', 'path' => null, 'json' => true],
|
||||||
'nginx_error' => ['label' => 'nginx — erreurs', 'path' => '/var/log/nginx/xamxam_error.log'],
|
'admin' => ['label' => 'Admin — actions', 'path' => null, 'json' => true],
|
||||||
'php_error' => ['label' => 'PHP-FPM — erreurs', 'path' => '/var/log/php8.4-fpm.log'],
|
'error' => ['label' => 'Erreurs — application', 'path' => null, 'json' => true],
|
||||||
|
'audit' => ['label' => 'Audit — données', 'path' => null, 'json' => true],
|
||||||
|
'nginx_access' => ['label' => 'nginx — accès', 'path' => '/var/log/nginx/xamxam_access.log', 'json' => false],
|
||||||
|
'nginx_error' => ['label' => 'nginx — erreurs','path' => '/var/log/nginx/xamxam_error.log', 'json' => false],
|
||||||
];
|
];
|
||||||
|
|
||||||
public const ALLOWED_LINES = [50, 100, 200, 500];
|
/**
|
||||||
|
* Resolve a log file path — app logs live under STORAGE_ROOT, system logs
|
||||||
|
* have hard-coded paths (only valid in production).
|
||||||
|
*/
|
||||||
|
private static function resolveLogPath(string $tab): string
|
||||||
|
{
|
||||||
|
$def = self::LOG_FILES[$tab];
|
||||||
|
if ($def['path'] !== null) {
|
||||||
|
return $def['path'];
|
||||||
|
}
|
||||||
|
// App logs: storage/logs/{channel}.log (Monolog RotatingFileHandler uses
|
||||||
|
// this as base name; the current log is always at {channel}-YYYY-MM-DD.log)
|
||||||
|
$dir = defined('STORAGE_ROOT') ? STORAGE_ROOT . '/logs' : APP_ROOT . '/storage/logs';
|
||||||
|
// Find the most recent dated log file for this channel
|
||||||
|
$base = $dir . '/' . $tab;
|
||||||
|
$dated = glob($base . '-20[0-9][0-9]-[0-9][0-9]-[0-9][0-9].log');
|
||||||
|
if (!empty($dated)) {
|
||||||
|
rsort($dated); // newest first
|
||||||
|
return $dated[0];
|
||||||
|
}
|
||||||
|
// Fall back to the bare name (pre-existing or non-rotated)
|
||||||
|
return $base . '.log';
|
||||||
|
}
|
||||||
|
|
||||||
/** Live deployed nginx config path. */
|
public const ALLOWED_LINES = [50, 100, 200, 500];
|
||||||
public const NGINX_CONFIG_LIVE = '/etc/nginx/sites-available/xamxam';
|
|
||||||
/** Local reference copy used as fallback in dev. */
|
|
||||||
public const NGINX_CONFIG_LOCAL = APP_ROOT . '/nginx/xamxam.conf';
|
|
||||||
|
|
||||||
// ── TTLs ──────────────────────────────────────────────────────────────────
|
// ── TTLs ──────────────────────────────────────────────────────────────────
|
||||||
private const TTL_STATUS = 120; // 2 minutes
|
private const TTL_STATUS = 120; // 2 minutes
|
||||||
@@ -140,11 +161,37 @@ class SystemController
|
|||||||
*/
|
*/
|
||||||
public function getLogData(string $tab, int $n): array
|
public function getLogData(string $tab, int $n): array
|
||||||
{
|
{
|
||||||
$logPath = self::LOG_FILES[$tab]['path'];
|
$logPath = self::resolveLogPath($tab);
|
||||||
|
$isJson = self::LOG_FILES[$tab]['json'] ?? false;
|
||||||
$error = null;
|
$error = null;
|
||||||
$lines = $this->readLogTail($logPath, $n, $error);
|
$rawLines = null;
|
||||||
$meta = null;
|
|
||||||
|
|
||||||
|
if (!file_exists($logPath)) {
|
||||||
|
// App logs are rotated by Monolog; a missing file just means no
|
||||||
|
// events have been logged yet. Show a friendly empty-state message
|
||||||
|
// instead of a scary "fichier introuvable" error.
|
||||||
|
if ($isJson) {
|
||||||
|
return [
|
||||||
|
'lines' => [],
|
||||||
|
'error' => null,
|
||||||
|
'meta' => null,
|
||||||
|
'isJson' => true,
|
||||||
|
'notYet' => true,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
// System logs genuinely missing — show an error
|
||||||
|
$error = 'Fichier introuvable : ' . htmlspecialchars($logPath);
|
||||||
|
} else {
|
||||||
|
$rawLines = $this->readLogTail($logPath, $n, $error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse JSON lines into display strings
|
||||||
|
$lines = null;
|
||||||
|
if ($rawLines !== null) {
|
||||||
|
$lines = $isJson ? self::formatJsonLines($rawLines) : $rawLines;
|
||||||
|
}
|
||||||
|
|
||||||
|
$meta = null;
|
||||||
if (file_exists($logPath)) {
|
if (file_exists($logPath)) {
|
||||||
$sz = filesize($logPath);
|
$sz = filesize($logPath);
|
||||||
$meta = [
|
$meta = [
|
||||||
@@ -156,43 +203,102 @@ class SystemController
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
return ['lines' => $lines, 'error' => $error, 'meta' => $meta];
|
return ['lines' => $lines, 'error' => $error, 'meta' => $meta, 'isJson' => $isJson, 'notYet' => false];
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Nginx config tab ──────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Read and return data for the nginx config tab.
|
* Format JSON log lines into human-readable display strings.
|
||||||
*
|
*
|
||||||
* @return array{lines: ?array, source: ?string, meta: ?array, error: ?string}
|
* Each line is a flat JSON object. We extract key fields and build a
|
||||||
|
* compact one-line representation with emoji status indicators.
|
||||||
|
*
|
||||||
|
* @param string[] $jsonLines Raw JSON strings (newest first).
|
||||||
|
* @return string[] Formatted display lines.
|
||||||
*/
|
*/
|
||||||
public function getNginxConfigData(): array
|
private static function formatJsonLines(array $jsonLines): array
|
||||||
{
|
{
|
||||||
$livePath = self::NGINX_CONFIG_LIVE;
|
$formatted = [];
|
||||||
$localPath = self::NGINX_CONFIG_LOCAL;
|
foreach ($jsonLines as $raw) {
|
||||||
|
$entry = json_decode($raw, true);
|
||||||
foreach ([[$livePath, 'live'], [$localPath, 'local']] as [$path, $src]) {
|
if (!is_array($entry)) {
|
||||||
if (file_exists($path) && is_readable($path)) {
|
$formatted[] = $raw; // not valid JSON — show raw
|
||||||
$raw = file($path, FILE_IGNORE_NEW_LINES);
|
continue;
|
||||||
if ($raw !== false) {
|
|
||||||
$sz = filesize($path);
|
|
||||||
$meta = [
|
|
||||||
'path' => $path,
|
|
||||||
'size' => $sz > 1048576
|
|
||||||
? number_format($sz / 1048576, 2) . ' MB'
|
|
||||||
: number_format($sz / 1024, 1) . ' KB',
|
|
||||||
'mtime' => date('d/m/Y H:i:s', filemtime($path)),
|
|
||||||
];
|
|
||||||
return ['lines' => $raw, 'source' => $src, 'meta' => $meta, 'error' => null];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$parts = [];
|
||||||
|
|
||||||
|
// Timestamp
|
||||||
|
$ts = $entry['timestamp'] ?? '';
|
||||||
|
if ($ts !== '') {
|
||||||
|
$parts[] = substr($ts, 0, 19); // YYYY-MM-DDTHH:MM:SS
|
||||||
|
}
|
||||||
|
|
||||||
|
// Status emoji
|
||||||
|
$status = $entry['status'] ?? '';
|
||||||
|
if ($status === 'success' || $status === 'active') {
|
||||||
|
$parts[] = '✓';
|
||||||
|
} elseif ($status === 'error' || $status === 'duplicate') {
|
||||||
|
$parts[] = '✗';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Key identifying fields (vary by channel)
|
||||||
|
$id = $entry['resource']
|
||||||
|
?? $entry['source']
|
||||||
|
?? $entry['context']
|
||||||
|
?? '';
|
||||||
|
if ($id !== '') {
|
||||||
|
$parts[] = $id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Action
|
||||||
|
$action = $entry['action'] ?? '';
|
||||||
|
if ($action !== '') {
|
||||||
|
$parts[] = $action;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Status text
|
||||||
|
if ($status !== '' && $status !== 'success') {
|
||||||
|
$parts[] = $status;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Actor / IP
|
||||||
|
$actor = $entry['actor'] ?? '';
|
||||||
|
if ($actor !== '') {
|
||||||
|
$parts[] = $actor;
|
||||||
|
} elseif (isset($entry['ip'])) {
|
||||||
|
$parts[] = $entry['ip'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// User agent (compact)
|
||||||
|
$ua = $entry['user_agent'] ?? '';
|
||||||
|
if ($ua !== '' && $ua !== 'unknown' && $ua !== 'cli') {
|
||||||
|
// Truncate UA: first meaningful segment
|
||||||
|
$uaShort = preg_match('#^([^(]+)#', $ua, $m) ? trim($m[1]) : $ua;
|
||||||
|
if (mb_strlen($uaShort) > 60) {
|
||||||
|
$uaShort = mb_substr($uaShort, 0, 57) . '…';
|
||||||
|
}
|
||||||
|
$parts[] = $uaShort;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For error log: exception + message
|
||||||
|
if (isset($entry['exception']) && isset($entry['message'])) {
|
||||||
|
$msg = $entry['exception'] . ': ' . $entry['message'];
|
||||||
|
if (mb_strlen($msg) > 120) {
|
||||||
|
$msg = mb_substr($msg, 0, 117) . '…';
|
||||||
|
}
|
||||||
|
$parts[] = $msg;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For audit log: table + record_id
|
||||||
|
$table = $entry['table'] ?? '';
|
||||||
|
if ($table !== '') {
|
||||||
|
$parts[] = $table . (isset($entry['record_id']) ? '#' . $entry['record_id'] : '');
|
||||||
|
}
|
||||||
|
|
||||||
|
$formatted[] = implode(' ', $parts);
|
||||||
}
|
}
|
||||||
|
|
||||||
$error = file_exists($livePath)
|
return $formatted;
|
||||||
? 'Fichier non lisible (permissions insuffisantes) : ' . htmlspecialchars($livePath)
|
|
||||||
: 'Config live introuvable (' . htmlspecialchars($livePath) . ') et config locale introuvable (' . htmlspecialchars($localPath) . ').';
|
|
||||||
|
|
||||||
return ['lines' => null, 'source' => null, 'meta' => null, 'error' => $error];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Line classifiers (used by both system.php and system-fragment.php) ────
|
// ── Line classifiers (used by both system.php and system-fragment.php) ────
|
||||||
@@ -223,21 +329,6 @@ class SystemController
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Return the CSS class for a nginx config line.
|
|
||||||
*/
|
|
||||||
public static function nginxLineClass(string $line): string
|
|
||||||
{
|
|
||||||
$trimmed = ltrim($line);
|
|
||||||
if ($trimmed === '' || str_starts_with($trimmed, '#')) {
|
|
||||||
return 'nginx-comment';
|
|
||||||
}
|
|
||||||
if (preg_match('/^\s*(location|server|upstream|events|http|geo|map|types)\b/', $line)) {
|
|
||||||
return 'nginx-block';
|
|
||||||
}
|
|
||||||
return 'nginx-directive';
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── View helpers ──────────────────────────────────────────────────────────
|
// ── View helpers ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -399,15 +490,14 @@ class SystemController
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Read the tail of a log file, newest-first. Returns null on error.
|
* Read the tail of a log file, newest-first. Returns null on error.
|
||||||
|
*
|
||||||
|
* Prefers tail(1) for efficiency on large files; falls back to a pure-PHP
|
||||||
|
* read for app log files when exec() is unavailable or tail fails.
|
||||||
*/
|
*/
|
||||||
private function readLogTail(string $logPath, int $n, ?string &$errorMsg): ?array
|
private function readLogTail(string $logPath, int $n, ?string &$errorMsg): ?array
|
||||||
{
|
{
|
||||||
$errorMsg = null;
|
$errorMsg = null;
|
||||||
|
|
||||||
if (!function_exists('exec')) {
|
|
||||||
$errorMsg = 'exec() est désactivé sur ce serveur.';
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (!file_exists($logPath)) {
|
if (!file_exists($logPath)) {
|
||||||
$errorMsg = 'Fichier introuvable : ' . htmlspecialchars($logPath);
|
$errorMsg = 'Fichier introuvable : ' . htmlspecialchars($logPath);
|
||||||
return null;
|
return null;
|
||||||
@@ -417,16 +507,25 @@ class SystemController
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
$output = [];
|
// Try tail(1) first — fast on large log files
|
||||||
$rc = 0;
|
if (function_exists('exec')) {
|
||||||
exec('tail -n ' . intval($n) . ' ' . escapeshellarg($logPath) . ' 2>/dev/null', $output, $rc);
|
$output = [];
|
||||||
|
$rc = 0;
|
||||||
|
@exec('tail -n ' . intval($n) . ' ' . escapeshellarg($logPath) . ' 2>/dev/null', $output, $rc);
|
||||||
|
if ($rc === 0) {
|
||||||
|
return array_reverse($output); // newest first
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if ($rc !== 0) {
|
// PHP fallback — reads the whole file, returns last N lines
|
||||||
|
$raw = @file($logPath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
||||||
|
if ($raw === false) {
|
||||||
$errorMsg = 'Erreur lors de la lecture du fichier journal.';
|
$errorMsg = 'Erreur lors de la lecture du fichier journal.';
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return array_reverse($output); // newest first
|
$slice = array_slice($raw, -min($n, count($raw)));
|
||||||
|
return array_reverse($slice);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -154,7 +154,7 @@ class ErrorHandler
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Write a structured error log entry.
|
* Write a structured error log entry — delegates to Monolog 'error' channel.
|
||||||
*
|
*
|
||||||
* @param string $context e.g. 'thesis_edit', 'partage_submit', 'csv_import'
|
* @param string $context e.g. 'thesis_edit', 'partage_submit', 'csv_import'
|
||||||
* @param \Throwable $e
|
* @param \Throwable $e
|
||||||
@@ -162,22 +162,22 @@ class ErrorHandler
|
|||||||
*/
|
*/
|
||||||
public static function log(string $context, \Throwable $e, array $extra = []): void
|
public static function log(string $context, \Throwable $e, array $extra = []): void
|
||||||
{
|
{
|
||||||
$parts = [
|
$entry = [
|
||||||
'context=' . $context,
|
'timestamp' => date('c'),
|
||||||
'exception=' . get_class($e),
|
'ip' => $_SERVER['REMOTE_ADDR'] ?? 'unknown',
|
||||||
'message=' . $e->getMessage(),
|
'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? '',
|
||||||
|
'context' => $context,
|
||||||
|
'exception' => get_class($e),
|
||||||
|
'message' => $e->getMessage(),
|
||||||
|
'trace' => $e->getTraceAsString(),
|
||||||
];
|
];
|
||||||
foreach ($extra as $k => $v) {
|
|
||||||
if (is_scalar($v) || $v === null) {
|
|
||||||
$parts[] = $k . '=' . json_encode($v, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
|
|
||||||
} elseif (is_array($v)) {
|
|
||||||
$parts[] = $k . '=' . json_encode($v, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
|
|
||||||
} else {
|
|
||||||
$parts[] = $k . '=' . gettype($v);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$parts[] = 'trace=' . $e->getTraceAsString();
|
|
||||||
|
|
||||||
error_log(implode(' | ', $parts));
|
if (!empty($extra)) {
|
||||||
|
$entry['extra'] = $extra;
|
||||||
|
}
|
||||||
|
|
||||||
|
$line = json_encode($entry, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
|
||||||
|
|
||||||
|
Logger::get('error')->error($line);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
105
app/src/Logger.php
Normal file
105
app/src/Logger.php
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Monolog\Handler\RotatingFileHandler;
|
||||||
|
use Monolog\Handler\NullHandler;
|
||||||
|
use Monolog\Formatter\LineFormatter;
|
||||||
|
use Monolog\Level;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Central logger factory — provides named Monolog channel instances
|
||||||
|
* backed by rotating JSON-line log files.
|
||||||
|
*
|
||||||
|
* Channels:
|
||||||
|
* - 'app' → replaces AppLogger
|
||||||
|
* - 'admin' → replaces AdminLogger file output
|
||||||
|
* - 'error' → replaces ErrorHandler logging
|
||||||
|
* - 'audit' → replaces Audit file shadow (DB writes stay in Audit)
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* Logger::get('app')->info('message', [...]);
|
||||||
|
* Logger::get('admin')->warning('message', [...]);
|
||||||
|
*/
|
||||||
|
class Logger
|
||||||
|
{
|
||||||
|
/** @var array<string, LoggerInterface> */
|
||||||
|
private static array $channels = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get (or lazily create) a named Monolog channel.
|
||||||
|
*/
|
||||||
|
public static function get(string $channel): LoggerInterface
|
||||||
|
{
|
||||||
|
if (!isset(self::$channels[$channel])) {
|
||||||
|
self::$channels[$channel] = self::create($channel);
|
||||||
|
}
|
||||||
|
return self::$channels[$channel];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a Monolog channel with rotating JSON file handler.
|
||||||
|
*
|
||||||
|
* Falls back to NullHandler if the log directory is not writable
|
||||||
|
* (e.g. CLI scripts on a machine that doesn't have the production path).
|
||||||
|
*/
|
||||||
|
private static function create(string $channel): \Monolog\Logger
|
||||||
|
{
|
||||||
|
$logDir = self::logDir();
|
||||||
|
|
||||||
|
if (!is_dir($logDir) && !@mkdir($logDir, 0755, true) && !is_dir($logDir)) {
|
||||||
|
// Directory can't be created — use null handler (graceful degradation)
|
||||||
|
$logger = new \Monolog\Logger($channel);
|
||||||
|
$logger->pushHandler(new NullHandler());
|
||||||
|
return $logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$handler = new RotatingFileHandler(
|
||||||
|
$logDir . '/' . $channel . '.log',
|
||||||
|
30, // keep 30 days of logs
|
||||||
|
self::level()
|
||||||
|
);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
// Can't open log file — use null handler
|
||||||
|
$logger = new \Monolog\Logger($channel);
|
||||||
|
$logger->pushHandler(new NullHandler());
|
||||||
|
return $logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pass-through formatter: the facades (AppLogger, AdminLogger, etc.)
|
||||||
|
// construct their own JSON lines and pass them as the log message.
|
||||||
|
// %message% preserves the existing JSON format contract exactly.
|
||||||
|
$handler->setFormatter(new LineFormatter("%message%\n", null, true));
|
||||||
|
|
||||||
|
$logger = new \Monolog\Logger($channel);
|
||||||
|
$logger->pushHandler($handler);
|
||||||
|
|
||||||
|
return $logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read the LOG_LEVEL env var with sensible defaults.
|
||||||
|
*/
|
||||||
|
private static function level(): Level
|
||||||
|
{
|
||||||
|
$level = strtoupper(getenv('LOG_LEVEL') ?: '');
|
||||||
|
|
||||||
|
// Default: WARNING in production (always set in .env), DEBUG otherwise
|
||||||
|
if ($level === '') {
|
||||||
|
return php_sapi_name() === 'cli-server' ? Level::Debug : Level::Warning;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Level::fromName($level);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve the log directory.
|
||||||
|
*/
|
||||||
|
private static function logDir(): string
|
||||||
|
{
|
||||||
|
if (defined('STORAGE_ROOT')) {
|
||||||
|
return STORAGE_ROOT . '/logs';
|
||||||
|
}
|
||||||
|
return __DIR__ . '/../storage/logs';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -472,23 +472,10 @@
|
|||||||
<?= htmlspecialchars($def['label']) ?>
|
<?= htmlspecialchars($def['label']) ?>
|
||||||
</a>
|
</a>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
<a href="?tab=nginx_config"
|
|
||||||
class="sys-tab <?= $activeTab === 'nginx_config' ? 'active' : '' ?>"
|
|
||||||
hx-get="/admin/system-fragment.php?tab=nginx_config"
|
|
||||||
hx-target="#sys-tab-panel"
|
|
||||||
hx-push-url="?tab=nginx_config"
|
|
||||||
hx-swap="innerHTML"
|
|
||||||
hx-indicator="#sys-tab-panel"
|
|
||||||
data-tab="nginx_config"
|
|
||||||
<?= $activeTab === 'nginx_config' ? 'aria-current="page"' : '' ?>>nginx — config</a>
|
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div id="sys-tab-panel">
|
<div id="sys-tab-panel">
|
||||||
<?php if ($activeTab === 'nginx_config'): ?>
|
<?php include APP_ROOT . '/templates/admin/partials/system-log-panel.php'; ?>
|
||||||
<?php include APP_ROOT . '/templates/admin/partials/system-nginx-config-panel.php'; ?>
|
|
||||||
<?php else: ?>
|
|
||||||
<?php include APP_ROOT . '/templates/admin/partials/system-log-panel.php'; ?>
|
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</article>
|
</article>
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
|
|
||||||
<?php if ($logFileMeta): ?>
|
<?php if ($logFileMeta): ?>
|
||||||
<div class="log-meta">
|
<div class="log-meta">
|
||||||
<span data-label="Fichier"><?= htmlspecialchars(SystemController::LOG_FILES[$activeTab]['path']) ?></span>
|
<span data-label="Fichier"><?= htmlspecialchars($logFileMeta['path']) ?></span>
|
||||||
<span data-label="Taille"><?= $logFileMeta['size'] ?></span>
|
<span data-label="Taille"><?= $logFileMeta['size'] ?></span>
|
||||||
<span data-label="Modifié"><?= $logFileMeta['mtime'] ?></span>
|
<span data-label="Modifié"><?= $logFileMeta['mtime'] ?></span>
|
||||||
</div>
|
</div>
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
<div class="log-unavailable">
|
<div class="log-unavailable">
|
||||||
<strong>Journaux non disponibles</strong>
|
<strong>Journaux non disponibles</strong>
|
||||||
<div class="log-unavail-path"><?= $logError ?></div>
|
<div class="log-unavail-path"><?= $logError ?></div>
|
||||||
<?php if (php_sapi_name() === 'cli-server'): ?>
|
<?php if (php_sapi_name() === 'cli-server' && str_starts_with($activeTab, 'nginx')): ?>
|
||||||
<div class="log-unavail-dev">
|
<div class="log-unavail-dev">
|
||||||
En environnement de développement, les logs nginx ne sont pas disponibles.
|
En environnement de développement, les logs nginx ne sont pas disponibles.
|
||||||
Cette page est pleinement fonctionnelle sur le serveur de production.
|
Cette page est pleinement fonctionnelle sur le serveur de production.
|
||||||
@@ -37,6 +37,12 @@
|
|||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<?php elseif (!empty($notYet)): ?>
|
||||||
|
<div class="log-empty">
|
||||||
|
Aucune entrée pour le moment.<br>
|
||||||
|
<span class="log-unavail-path">Le journal sera créé automatiquement au premier événement.</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<?php elseif (empty($logLines)): ?>
|
<?php elseif (empty($logLines)): ?>
|
||||||
<div class="log-empty">Le fichier journal est vide.</div>
|
<div class="log-empty">Le fichier journal est vide.</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,40 +0,0 @@
|
|||||||
<?php if ($nginxConfigMeta): ?>
|
|
||||||
<div class="log-meta">
|
|
||||||
<span data-label="Fichier"><?= htmlspecialchars($nginxConfigMeta['path']) ?></span>
|
|
||||||
<span data-label="Taille"><?= $nginxConfigMeta['size'] ?></span>
|
|
||||||
<span data-label="Modifié"><?= $nginxConfigMeta['mtime'] ?></span>
|
|
||||||
<?php if ($nginxConfigSource === 'live'): ?>
|
|
||||||
<span class="nginx-source-badge nginx-source-badge--live">● Config déployée</span>
|
|
||||||
<?php else: ?>
|
|
||||||
<span class="nginx-source-badge nginx-source-badge--local">⚠ Référence locale (config live inaccessible)</span>
|
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<?php if ($nginxConfigError !== null): ?>
|
|
||||||
<div class="log-unavailable">
|
|
||||||
<strong>Configuration nginx non disponible</strong>
|
|
||||||
<div class="log-unavail-path"><?= htmlspecialchars($nginxConfigError) ?></div>
|
|
||||||
<?php if (php_sapi_name() === 'cli-server'): ?>
|
|
||||||
<div class="log-unavail-dev">
|
|
||||||
En développement, <code>/etc/nginx/sites-available/xamxam</code> n'existe pas.
|
|
||||||
La config de référence se trouve dans <code>nginx/xamxam.conf</code>.
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<?php elseif (empty($nginxConfigLines)): ?>
|
|
||||||
<div class="log-empty">Le fichier de configuration est vide.</div>
|
|
||||||
|
|
||||||
<?php else: ?>
|
|
||||||
<div class="log-output" id="log-output" role="region" aria-label="Configuration nginx">
|
|
||||||
<button class="btn btn--secondary btn--sm log-copy-btn" id="log-copy-btn" type="button" title="Copier la configuration"
|
|
||||||
onclick="copyLogContent(this);return false">
|
|
||||||
Copier
|
|
||||||
</button>
|
|
||||||
<?php foreach ($nginxConfigLines as $i => $line): ?>
|
|
||||||
<span class="log-line <?= SystemController::nginxLineClass($line) ?>"
|
|
||||||
data-n="<?= $i + 1 ?>"><?= htmlspecialchars($line, ENT_QUOTES | ENT_SUBSTITUTE) ?></span>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
@@ -1,114 +0,0 @@
|
|||||||
<main id="main-content">
|
|
||||||
<h1>Système</h1>
|
|
||||||
|
|
||||||
<p class="sys-refresh-note">
|
|
||||||
Affiché le <?= date('d/m/Y à H:i:s') ?> —
|
|
||||||
<a href="?tab=<?= htmlspecialchars($activeTab) ?>&n=<?= $selectedN ?>">Rafraîchir</a> —
|
|
||||||
<a href="?tab=<?= htmlspecialchars($activeTab) ?>&n=<?= $selectedN ?>&refresh=1">Forcer actualisation</a>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<!-- ════════════════════════════════════════════════════════════════════
|
|
||||||
STATUS SECTION — always visible above tabs
|
|
||||||
════════════════════════════════════════════════════════════════════ -->
|
|
||||||
<section class="sys-status-section" aria-label="Statut du système">
|
|
||||||
<div class="sys-status-header">
|
|
||||||
<h2 class="srv-section-title srv-section-title--compact">Statut
|
|
||||||
<?php if ($statusCached && $statusCacheAge !== null): ?>
|
|
||||||
<span class="sys-cache-badge sys-cache-badge--hit" title="Données en cache">
|
|
||||||
⚡ Cache — il y a <?= $statusCacheAge ?>s
|
|
||||||
</span>
|
|
||||||
<?php else: ?>
|
|
||||||
<span class="sys-cache-badge sys-cache-badge--miss" title="Données fraîches">
|
|
||||||
⟳ Actualisé
|
|
||||||
</span>
|
|
||||||
<?php endif; ?>
|
|
||||||
</h2>
|
|
||||||
<button id="sys-status-toggle" class="sys-status-toggle"
|
|
||||||
aria-expanded="<?= $statusInitiallyCollapsed ? 'false' : 'true' ?>" aria-controls="sys-status-body"
|
|
||||||
type="button"
|
|
||||||
onclick="var b=document.getElementById('sys-status-body');var c=b.hidden;b.hidden=!c;this.setAttribute('aria-expanded',c);this.textContent=c?'▲ Réduire':'▼ Développer';document.cookie='sys_collapsed='+(!c)+';path=/;max-age=31536000';return false">
|
|
||||||
<?= $statusInitiallyCollapsed ? '▼ Développer' : '▲ Réduire' ?>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="sys-status-body"<?= $statusInitiallyCollapsed ? ' hidden' : '' ?>>
|
|
||||||
<div class="srv-grid">
|
|
||||||
<?php foreach ($checks as $check): ?>
|
|
||||||
<?php $st = $check['status'] ?? 'unknown'; ?>
|
|
||||||
<div class="srv-card">
|
|
||||||
<div class="srv-card__header">
|
|
||||||
<span class="srv-card__name"><?= htmlspecialchars($check['label']) ?></span>
|
|
||||||
<span class="<?= SystemController::statusClass($st) ?>"><?= SystemController::statusLabel($st) ?></span>
|
|
||||||
</div>
|
|
||||||
<?php if (!empty($check['detail'])): ?>
|
|
||||||
<div class="srv-card__detail"><?= htmlspecialchars($check['detail']) ?></div>
|
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="sys-status-meta">
|
|
||||||
<div>
|
|
||||||
<h3 class="srv-section-title srv-section-title--sub">Environnement PHP</h3>
|
|
||||||
<div class="php-grid php-grid--flush">
|
|
||||||
<?php foreach ($phpInfo as $key => $val): ?>
|
|
||||||
<div class="php-item">
|
|
||||||
<div class="php-item__key"><?= htmlspecialchars($key) ?></div>
|
|
||||||
<div class="php-item__val"><?= htmlspecialchars($val) ?></div>
|
|
||||||
</div>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h3 class="srv-section-title srv-section-title--sub">Espace disque</h3>
|
|
||||||
<div class="disk-bar-wrap">
|
|
||||||
<div class="disk-bar" style="--disk-pct:<?= $diskPct ?>%;--disk-color:<?= $diskColor ?>"></div>
|
|
||||||
</div>
|
|
||||||
<div class="disk-stats">
|
|
||||||
<span><?= SystemController::humanBytes($diskUsed) ?> utilisé (<?= $diskPct ?>%)</span>
|
|
||||||
<span><?= SystemController::humanBytes($diskFree) ?> libre / <?= SystemController::humanBytes($diskTotal) ?></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- ── Tab bar ─────────────────────────────────────────────────────── -->
|
|
||||||
<nav class="sys-tabs" aria-label="Journaux et configuration">
|
|
||||||
<?php foreach (SystemController::LOG_FILES as $key => $def): ?>
|
|
||||||
<a href="?tab=<?= htmlspecialchars($key) ?>&n=<?= $selectedN ?>"
|
|
||||||
class="sys-tab <?= $activeTab === $key ? 'active' : '' ?>"
|
|
||||||
hx-get="/admin/system-fragment.php?tab=<?= htmlspecialchars($key) ?>&n=<?= $selectedN ?>"
|
|
||||||
hx-target="#sys-tab-panel"
|
|
||||||
hx-push-url="?tab=<?= htmlspecialchars($key) ?>&n=<?= $selectedN ?>"
|
|
||||||
hx-swap="innerHTML"
|
|
||||||
hx-indicator="#sys-tab-panel"
|
|
||||||
data-tab="<?= htmlspecialchars($key) ?>"
|
|
||||||
<?= $activeTab === $key ? 'aria-current="page"' : '' ?>>
|
|
||||||
<?= htmlspecialchars($def['label']) ?>
|
|
||||||
</a>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
<a href="?tab=nginx_config"
|
|
||||||
class="sys-tab <?= $activeTab === 'nginx_config' ? 'active' : '' ?>"
|
|
||||||
hx-get="/admin/system-fragment.php?tab=nginx_config"
|
|
||||||
hx-target="#sys-tab-panel"
|
|
||||||
hx-push-url="?tab=nginx_config"
|
|
||||||
hx-swap="innerHTML"
|
|
||||||
hx-indicator="#sys-tab-panel"
|
|
||||||
data-tab="nginx_config"
|
|
||||||
<?= $activeTab === 'nginx_config' ? 'aria-current="page"' : '' ?>>nginx — config</a>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<!-- Tab panel — content swapped by HTMX -->
|
|
||||||
<div id="sys-tab-panel">
|
|
||||||
|
|
||||||
<?php if ($activeTab === 'nginx_config'): ?>
|
|
||||||
<?php include APP_ROOT . '/templates/admin/partials/system-nginx-config-panel.php'; ?>
|
|
||||||
<?php else: ?>
|
|
||||||
<?php include APP_ROOT . '/templates/admin/partials/system-log-panel.php'; ?>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
</div><!-- #sys-tab-panel -->
|
|
||||||
|
|
||||||
</main>
|
|
||||||
<script src="/assets/js/app/admin-logs.js"></script>
|
|
||||||
@@ -6,10 +6,11 @@
|
|||||||
"require": {
|
"require": {
|
||||||
"php": ">=8.4",
|
"php": ">=8.4",
|
||||||
"ext-json": "*",
|
"ext-json": "*",
|
||||||
"ext-pdo": "*",
|
|
||||||
"ext-openssl": "*",
|
"ext-openssl": "*",
|
||||||
"league/commonmark": "^2.4",
|
"ext-pdo": "*",
|
||||||
"guzzlehttp/guzzle": "^7.9",
|
"guzzlehttp/guzzle": "^7.9",
|
||||||
|
"league/commonmark": "^2.4",
|
||||||
|
"monolog/monolog": "^3.10",
|
||||||
"phpmailer/phpmailer": "^6.9"
|
"phpmailer/phpmailer": "^6.9"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
|
|||||||
209
composer.lock
generated
209
composer.lock
generated
@@ -4,7 +4,7 @@
|
|||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "14e1f81f7d65a5f6cf56c604bb12847d",
|
"content-hash": "b03509a0cf5bec5df187ca91f13c2f6a",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "dflydev/dot-access-data",
|
"name": "dflydev/dot-access-data",
|
||||||
@@ -597,6 +597,109 @@
|
|||||||
],
|
],
|
||||||
"time": "2022-12-11T20:36:23+00:00"
|
"time": "2022-12-11T20:36:23+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "monolog/monolog",
|
||||||
|
"version": "3.10.0",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/Seldaek/monolog.git",
|
||||||
|
"reference": "b321dd6749f0bf7189444158a3ce785cc16d69b0"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/Seldaek/monolog/zipball/b321dd6749f0bf7189444158a3ce785cc16d69b0",
|
||||||
|
"reference": "b321dd6749f0bf7189444158a3ce785cc16d69b0",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": ">=8.1",
|
||||||
|
"psr/log": "^2.0 || ^3.0"
|
||||||
|
},
|
||||||
|
"provide": {
|
||||||
|
"psr/log-implementation": "3.0.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"aws/aws-sdk-php": "^3.0",
|
||||||
|
"doctrine/couchdb": "~1.0@dev",
|
||||||
|
"elasticsearch/elasticsearch": "^7 || ^8",
|
||||||
|
"ext-json": "*",
|
||||||
|
"graylog2/gelf-php": "^1.4.2 || ^2.0",
|
||||||
|
"guzzlehttp/guzzle": "^7.4.5",
|
||||||
|
"guzzlehttp/psr7": "^2.2",
|
||||||
|
"mongodb/mongodb": "^1.8 || ^2.0",
|
||||||
|
"php-amqplib/php-amqplib": "~2.4 || ^3",
|
||||||
|
"php-console/php-console": "^3.1.8",
|
||||||
|
"phpstan/phpstan": "^2",
|
||||||
|
"phpstan/phpstan-deprecation-rules": "^2",
|
||||||
|
"phpstan/phpstan-strict-rules": "^2",
|
||||||
|
"phpunit/phpunit": "^10.5.17 || ^11.0.7",
|
||||||
|
"predis/predis": "^1.1 || ^2",
|
||||||
|
"rollbar/rollbar": "^4.0",
|
||||||
|
"ruflin/elastica": "^7 || ^8",
|
||||||
|
"symfony/mailer": "^5.4 || ^6",
|
||||||
|
"symfony/mime": "^5.4 || ^6"
|
||||||
|
},
|
||||||
|
"suggest": {
|
||||||
|
"aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB",
|
||||||
|
"doctrine/couchdb": "Allow sending log messages to a CouchDB server",
|
||||||
|
"elasticsearch/elasticsearch": "Allow sending log messages to an Elasticsearch server via official client",
|
||||||
|
"ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)",
|
||||||
|
"ext-curl": "Required to send log messages using the IFTTTHandler, the LogglyHandler, the SendGridHandler, the SlackWebhookHandler or the TelegramBotHandler",
|
||||||
|
"ext-mbstring": "Allow to work properly with unicode symbols",
|
||||||
|
"ext-mongodb": "Allow sending log messages to a MongoDB server (via driver)",
|
||||||
|
"ext-openssl": "Required to send log messages using SSL",
|
||||||
|
"ext-sockets": "Allow sending log messages to a Syslog server (via UDP driver)",
|
||||||
|
"graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server",
|
||||||
|
"mongodb/mongodb": "Allow sending log messages to a MongoDB server (via library)",
|
||||||
|
"php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib",
|
||||||
|
"rollbar/rollbar": "Allow sending log messages to Rollbar",
|
||||||
|
"ruflin/elastica": "Allow sending log messages to an Elastic Search server"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"extra": {
|
||||||
|
"branch-alias": {
|
||||||
|
"dev-main": "3.x-dev"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Monolog\\": "src/Monolog"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Jordi Boggiano",
|
||||||
|
"email": "j.boggiano@seld.be",
|
||||||
|
"homepage": "https://seld.be"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Sends your logs to files, sockets, inboxes, databases and various web services",
|
||||||
|
"homepage": "https://github.com/Seldaek/monolog",
|
||||||
|
"keywords": [
|
||||||
|
"log",
|
||||||
|
"logging",
|
||||||
|
"psr-3"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/Seldaek/monolog/issues",
|
||||||
|
"source": "https://github.com/Seldaek/monolog/tree/3.10.0"
|
||||||
|
},
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://github.com/Seldaek",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://tidelift.com/funding/github/packagist/monolog/monolog",
|
||||||
|
"type": "tidelift"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": "2026-01-02T08:56:05+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "nette/schema",
|
"name": "nette/schema",
|
||||||
"version": "v1.3.5",
|
"version": "v1.3.5",
|
||||||
@@ -1046,6 +1149,56 @@
|
|||||||
},
|
},
|
||||||
"time": "2023-04-04T09:54:51+00:00"
|
"time": "2023-04-04T09:54:51+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "psr/log",
|
||||||
|
"version": "3.0.2",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/php-fig/log.git",
|
||||||
|
"reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3",
|
||||||
|
"reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": ">=8.0.0"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"extra": {
|
||||||
|
"branch-alias": {
|
||||||
|
"dev-master": "3.x-dev"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Psr\\Log\\": "src"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "PHP-FIG",
|
||||||
|
"homepage": "https://www.php-fig.org/"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Common interface for logging libraries",
|
||||||
|
"homepage": "https://github.com/php-fig/log",
|
||||||
|
"keywords": [
|
||||||
|
"log",
|
||||||
|
"psr",
|
||||||
|
"psr-3"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"source": "https://github.com/php-fig/log/tree/3.0.2"
|
||||||
|
},
|
||||||
|
"time": "2024-09-11T13:17:53+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "ralouphie/getallheaders",
|
"name": "ralouphie/getallheaders",
|
||||||
"version": "3.0.3",
|
"version": "3.0.3",
|
||||||
@@ -2614,56 +2767,6 @@
|
|||||||
},
|
},
|
||||||
"time": "2021-11-05T16:47:00+00:00"
|
"time": "2021-11-05T16:47:00+00:00"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "psr/log",
|
|
||||||
"version": "3.0.2",
|
|
||||||
"source": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "https://github.com/php-fig/log.git",
|
|
||||||
"reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3"
|
|
||||||
},
|
|
||||||
"dist": {
|
|
||||||
"type": "zip",
|
|
||||||
"url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3",
|
|
||||||
"reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3",
|
|
||||||
"shasum": ""
|
|
||||||
},
|
|
||||||
"require": {
|
|
||||||
"php": ">=8.0.0"
|
|
||||||
},
|
|
||||||
"type": "library",
|
|
||||||
"extra": {
|
|
||||||
"branch-alias": {
|
|
||||||
"dev-master": "3.x-dev"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"autoload": {
|
|
||||||
"psr-4": {
|
|
||||||
"Psr\\Log\\": "src"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"notification-url": "https://packagist.org/downloads/",
|
|
||||||
"license": [
|
|
||||||
"MIT"
|
|
||||||
],
|
|
||||||
"authors": [
|
|
||||||
{
|
|
||||||
"name": "PHP-FIG",
|
|
||||||
"homepage": "https://www.php-fig.org/"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"description": "Common interface for logging libraries",
|
|
||||||
"homepage": "https://github.com/php-fig/log",
|
|
||||||
"keywords": [
|
|
||||||
"log",
|
|
||||||
"psr",
|
|
||||||
"psr-3"
|
|
||||||
],
|
|
||||||
"support": {
|
|
||||||
"source": "https://github.com/php-fig/log/tree/3.0.2"
|
|
||||||
},
|
|
||||||
"time": "2024-09-11T13:17:53+00:00"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "react/cache",
|
"name": "react/cache",
|
||||||
"version": "v1.2.0",
|
"version": "v1.2.0",
|
||||||
@@ -5638,8 +5741,8 @@
|
|||||||
"platform": {
|
"platform": {
|
||||||
"php": ">=8.4",
|
"php": ">=8.4",
|
||||||
"ext-json": "*",
|
"ext-json": "*",
|
||||||
"ext-pdo": "*",
|
"ext-openssl": "*",
|
||||||
"ext-openssl": "*"
|
"ext-pdo": "*"
|
||||||
},
|
},
|
||||||
"platform-dev": {},
|
"platform-dev": {},
|
||||||
"platform-overrides": {
|
"platform-overrides": {
|
||||||
|
|||||||
141
docs/monolog-plan.md
Normal file
141
docs/monolog-plan.md
Normal 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
|
||||||
@@ -4,7 +4,7 @@ use PHPUnit\Framework\TestCase;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* SystemControllerHelpersTest — Pure logic tests for the static helper methods
|
* SystemControllerHelpersTest — Pure logic tests for the static helper methods
|
||||||
* in SystemController (humanBytes, diskColor, logLineClass, nginxLineClass,
|
* in SystemController (humanBytes, diskColor, logLineClass,
|
||||||
* statusLabel, statusClass).
|
* statusLabel, statusClass).
|
||||||
*
|
*
|
||||||
* These are stateless, no-IO functions.
|
* These are stateless, no-IO functions.
|
||||||
@@ -116,32 +116,6 @@ class SystemControllerHelpersTest extends TestCase
|
|||||||
$this->assertSame('', SystemController::logLineClass('GET / " 200 123 "'));
|
$this->assertSame('', SystemController::logLineClass('GET / " 200 123 "'));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── nginxLineClass() ──────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
public function testNginxLineClassComment(): void
|
|
||||||
{
|
|
||||||
$this->assertSame('nginx-comment', SystemController::nginxLineClass('# comment'));
|
|
||||||
$this->assertSame('nginx-comment', SystemController::nginxLineClass(' # indented comment'));
|
|
||||||
$this->assertSame('nginx-comment', SystemController::nginxLineClass(''));
|
|
||||||
$this->assertSame('nginx-comment', SystemController::nginxLineClass(' '));
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testNginxLineClassBlock(): void
|
|
||||||
{
|
|
||||||
$this->assertSame('nginx-block', SystemController::nginxLineClass('server {'));
|
|
||||||
$this->assertSame('nginx-block', SystemController::nginxLineClass('location / {'));
|
|
||||||
$this->assertSame('nginx-block', SystemController::nginxLineClass(' upstream backend {'));
|
|
||||||
$this->assertSame('nginx-block', SystemController::nginxLineClass('events {'));
|
|
||||||
$this->assertSame('nginx-block', SystemController::nginxLineClass('http {'));
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testNginxLineClassDirective(): void
|
|
||||||
{
|
|
||||||
$this->assertSame('nginx-directive', SystemController::nginxLineClass('listen 80;'));
|
|
||||||
$this->assertSame('nginx-directive', SystemController::nginxLineClass('root /var/www;'));
|
|
||||||
$this->assertSame('nginx-directive', SystemController::nginxLineClass(' server_name example.com;'));
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── statusLabel() ─────────────────────────────────────────────────────────
|
// ── statusLabel() ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
public function testStatusLabelActive(): void
|
public function testStatusLabelActive(): void
|
||||||
|
|||||||
Reference in New Issue
Block a user