mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-05-06 11:09:18 +02:00
- public/admin/logs.php: new page tailing nginx error/access + PHP-FPM logs. Selector for log file and line count (50/100/200/500, default 100). Lines reversed (newest first), colour-coded by severity, numbered gutter. Graceful degradation when exec() unavailable or file unreadable (dev msg). - templates/admin/head.php: 'Journaux' nav link added after 'Statut'. - public/admin/status.php: remove curl_close() call deprecated in PHP 8.5 (no-op since PHP 8.0); replace with unset($ch) to silence the warning that was leaking raw text above the page output.
280 lines
9.8 KiB
PHP
280 lines
9.8 KiB
PHP
<?php
|
|
require_once __DIR__ . "/../../config/bootstrap.php";
|
|
require_once __DIR__ . '/../../src/AdminAuth.php';
|
|
AdminAuth::requireLogin();
|
|
|
|
$pageTitle = "Journaux serveur";
|
|
|
|
// ── Log file definitions ──────────────────────────────────────────────────────
|
|
// Paths match the nginx config (posterg_access.log / posterg_error.log).
|
|
// On the dev server these files won't exist; we handle that gracefully.
|
|
const LOG_FILES = [
|
|
'nginx_error' => ['label' => 'nginx — erreurs', 'path' => '/var/log/nginx/posterg_error.log'],
|
|
'nginx_access' => ['label' => 'nginx — accès', 'path' => '/var/log/nginx/posterg_access.log'],
|
|
'php_error' => ['label' => 'PHP-FPM — erreurs', 'path' => '/var/log/php8.4-fpm.log'],
|
|
];
|
|
|
|
const ALLOWED_LINES = [50, 100, 200, 500];
|
|
|
|
// ── Input validation ──────────────────────────────────────────────────────────
|
|
$selectedKey = $_GET['log'] ?? 'nginx_error';
|
|
$selectedN = isset($_GET['n']) ? (int) $_GET['n'] : 100;
|
|
|
|
if (!array_key_exists($selectedKey, LOG_FILES)) {
|
|
$selectedKey = 'nginx_error';
|
|
}
|
|
if (!in_array($selectedN, ALLOWED_LINES, true)) {
|
|
$selectedN = 100;
|
|
}
|
|
|
|
$logDef = LOG_FILES[$selectedKey];
|
|
$logPath = $logDef['path'];
|
|
|
|
// ── Read log ──────────────────────────────────────────────────────────────────
|
|
$lines = null; // null = unavailable, [] = empty, [...] = lines
|
|
$readError = null;
|
|
|
|
if (!function_exists('exec')) {
|
|
$readError = "exec() est désactivé sur ce serveur.";
|
|
} elseif (!file_exists($logPath)) {
|
|
$readError = "Fichier introuvable : " . htmlspecialchars($logPath);
|
|
} elseif (!is_readable($logPath)) {
|
|
$readError = "Fichier non lisible (permissions insuffisantes) : " . htmlspecialchars($logPath);
|
|
} else {
|
|
$output = [];
|
|
$rc = 0;
|
|
// tail -n N ensures we never load the whole file into memory
|
|
exec('tail -n ' . intval($selectedN) . ' ' . escapeshellarg($logPath) . ' 2>/dev/null', $output, $rc);
|
|
if ($rc !== 0) {
|
|
$readError = "Erreur lors de la lecture du fichier journal.";
|
|
} else {
|
|
// Reverse so newest lines appear first
|
|
$lines = array_reverse($output);
|
|
}
|
|
}
|
|
|
|
// ── File metadata ─────────────────────────────────────────────────────────────
|
|
$fileMeta = null;
|
|
if (file_exists($logPath)) {
|
|
$size = filesize($logPath);
|
|
$mtime = filemtime($logPath);
|
|
$fileMeta = [
|
|
'size' => $size > 1048576
|
|
? number_format($size / 1048576, 2) . ' MB'
|
|
: number_format($size / 1024, 1) . ' KB',
|
|
'mtime' => date('d/m/Y H:i:s', $mtime),
|
|
];
|
|
}
|
|
|
|
// ── Log-level classifier (for nginx error log coloring) ───────────────────────
|
|
/**
|
|
* Returns a CSS class based on the log level token in a log line.
|
|
* nginx error lines look like: 2024/01/15 12:34:56 [error] 1234#1234: …
|
|
* nginx access lines: 1.2.3.4 - - [15/Jan/2024:12:34:56 +0000] "GET / HTTP/1.1" 200 …
|
|
*/
|
|
function logLineClass(string $line): string {
|
|
// nginx error log: [crit], [error], [warn], [notice], [info], [debug]
|
|
if (preg_match('/\[(crit|emerg|alert)\]/', $line)) return 'log-crit';
|
|
if (preg_match('/\[error\]/', $line)) return 'log-error';
|
|
if (preg_match('/\[warn\]/', $line)) return 'log-warn';
|
|
if (preg_match('/\[notice\]/', $line)) return 'log-notice';
|
|
// access log: highlight HTTP 4xx / 5xx
|
|
if (preg_match('/" [45]\d\d /', $line)) return 'log-error';
|
|
if (preg_match('/" 3\d\d /', $line)) return 'log-notice';
|
|
return '';
|
|
}
|
|
|
|
require_once APP_ROOT . '/templates/admin/head.php';
|
|
?>
|
|
|
|
<style>
|
|
/* ── Log viewer layout ─────────────────────────────────────────────────── */
|
|
.log-toolbar {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
align-items: center;
|
|
gap: .6rem;
|
|
margin-bottom: 1.25rem;
|
|
}
|
|
.log-toolbar select,
|
|
.log-toolbar input[type=submit] {
|
|
background: var(--admin-bg-alt);
|
|
border: 1px solid var(--admin-border);
|
|
color: var(--admin-text);
|
|
border-radius: 4px;
|
|
padding: .4rem .7rem;
|
|
font-size: .85rem;
|
|
font-family: inherit;
|
|
}
|
|
.log-toolbar select:focus { outline: 2px solid var(--admin-purple); }
|
|
.log-toolbar input[type=submit] {
|
|
cursor: pointer;
|
|
background: var(--admin-purple);
|
|
border-color: var(--admin-purple);
|
|
color: #fff;
|
|
font-weight: 600;
|
|
}
|
|
.log-toolbar input[type=submit]:hover { opacity: .85; }
|
|
|
|
.log-meta {
|
|
font-size: .78rem;
|
|
color: var(--admin-text-muted);
|
|
font-family: ui-monospace, "SFMono-Regular", Consolas, monospace;
|
|
margin-bottom: .75rem;
|
|
display: flex;
|
|
gap: 1.5rem;
|
|
flex-wrap: wrap;
|
|
}
|
|
.log-meta span::before { content: attr(data-label) ": "; opacity: .6; }
|
|
|
|
.log-unavailable {
|
|
background: var(--admin-bg-alt);
|
|
border: 1px solid var(--admin-border);
|
|
border-radius: 4px;
|
|
padding: 1.5rem;
|
|
color: var(--admin-text-muted);
|
|
font-size: .88rem;
|
|
}
|
|
.log-unavailable .log-unavail-path {
|
|
font-family: ui-monospace, "SFMono-Regular", Consolas, monospace;
|
|
font-size: .8rem;
|
|
margin-top: .4rem;
|
|
opacity: .7;
|
|
}
|
|
|
|
.log-empty {
|
|
color: var(--admin-text-muted);
|
|
font-size: .88rem;
|
|
padding: 1rem 0;
|
|
}
|
|
|
|
/* Log output block */
|
|
.log-output {
|
|
background: #0d0d0d;
|
|
border: 1px solid var(--admin-border);
|
|
border-radius: 4px;
|
|
padding: 1rem;
|
|
overflow-x: auto;
|
|
font-family: ui-monospace, "SFMono-Regular", Consolas, "Courier New", monospace;
|
|
font-size: .76rem;
|
|
line-height: 1.55;
|
|
max-height: 72vh;
|
|
overflow-y: auto;
|
|
/* Reverse scroll so newest (first in DOM) is at top */
|
|
}
|
|
|
|
.log-line {
|
|
display: block;
|
|
white-space: pre;
|
|
padding: .05rem .1rem;
|
|
border-radius: 2px;
|
|
color: #bbb;
|
|
}
|
|
.log-line + .log-line { border-top: 1px solid rgba(255,255,255,.03); }
|
|
|
|
.log-crit { color: #ff7070; background: rgba(200,0,0,.12); }
|
|
.log-error { color: #f08080; }
|
|
.log-warn { color: #ffd080; }
|
|
.log-notice { color: #a0c8ff; }
|
|
|
|
/* line number gutter */
|
|
.log-line::before {
|
|
content: attr(data-n);
|
|
display: inline-block;
|
|
min-width: 4ch;
|
|
margin-right: .75rem;
|
|
opacity: .3;
|
|
text-align: right;
|
|
user-select: none;
|
|
}
|
|
|
|
.log-count-badge {
|
|
background: var(--admin-bg-alt);
|
|
border: 1px solid var(--admin-border);
|
|
border-radius: 3px;
|
|
font-size: .76rem;
|
|
padding: .15rem .5rem;
|
|
color: var(--admin-text-muted);
|
|
font-family: ui-monospace, monospace;
|
|
}
|
|
.log-refresh-note {
|
|
font-size: .78rem;
|
|
color: var(--admin-text-muted);
|
|
margin-bottom: 1.25rem;
|
|
}
|
|
.log-refresh-note a { color: var(--admin-purple); text-decoration: none; }
|
|
.log-refresh-note a:hover { text-decoration: underline; }
|
|
</style>
|
|
|
|
<main class="admin-main">
|
|
<h1 class="admin-page-title">Journaux serveur</h1>
|
|
|
|
<p class="log-refresh-note">
|
|
Affiché le <?= date('d/m/Y à H:i:s') ?> —
|
|
<a href="?log=<?= htmlspecialchars($selectedKey) ?>&n=<?= $selectedN ?>">Rafraîchir</a>
|
|
</p>
|
|
|
|
<!-- Toolbar: log selector + line count -->
|
|
<form class="log-toolbar" method="get" action="">
|
|
<select name="log" aria-label="Fichier journal">
|
|
<?php foreach (LOG_FILES as $key => $def): ?>
|
|
<option value="<?= htmlspecialchars($key) ?>"
|
|
<?= $key === $selectedKey ? 'selected' : '' ?>>
|
|
<?= htmlspecialchars($def['label']) ?>
|
|
</option>
|
|
<?php endforeach; ?>
|
|
</select>
|
|
|
|
<select name="n" aria-label="Nombre de lignes">
|
|
<?php foreach (ALLOWED_LINES as $n): ?>
|
|
<option value="<?= $n ?>" <?= $n === $selectedN ? 'selected' : '' ?>>
|
|
<?= $n ?> dernières lignes
|
|
</option>
|
|
<?php endforeach; ?>
|
|
</select>
|
|
|
|
<input type="submit" value="Afficher">
|
|
|
|
<?php if ($lines !== null && count($lines) > 0): ?>
|
|
<span class="log-count-badge"><?= count($lines) ?> ligne(s)</span>
|
|
<?php endif; ?>
|
|
</form>
|
|
|
|
<!-- File metadata -->
|
|
<?php if ($fileMeta): ?>
|
|
<div class="log-meta">
|
|
<span data-label="Fichier"><?= htmlspecialchars($logPath) ?></span>
|
|
<span data-label="Taille"><?= $fileMeta['size'] ?></span>
|
|
<span data-label="Modifié"><?= $fileMeta['mtime'] ?></span>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<!-- Output -->
|
|
<?php if ($readError !== null): ?>
|
|
<div class="log-unavailable">
|
|
<strong>Journaux non disponibles</strong>
|
|
<div class="log-unavail-path"><?= $readError ?></div>
|
|
<?php if (php_sapi_name() === 'cli-server'): ?>
|
|
<div style="margin-top:.6rem;font-size:.8rem;opacity:.7;">
|
|
En environnement de développement, les logs nginx ne sont pas disponibles.
|
|
Cette page est pleinement fonctionnelle sur le serveur de production.
|
|
</div>
|
|
<?php endif; ?>
|
|
</div>
|
|
|
|
<?php elseif (empty($lines)): ?>
|
|
<div class="log-empty">Le fichier journal est vide.</div>
|
|
|
|
<?php else: ?>
|
|
<div class="log-output" role="log" aria-live="off" aria-label="Contenu du journal">
|
|
<?php foreach ($lines as $i => $line): ?>
|
|
<span class="log-line <?= logLineClass($line) ?>"
|
|
data-n="<?= count($lines) - $i ?>"><?= htmlspecialchars($line, ENT_QUOTES | ENT_SUBSTITUTE) ?></span>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
</main>
|
|
|
|
<?php require_once APP_ROOT . '/templates/admin/footer.php'; ?>
|