Get detail logs page working - WIP

This commit is contained in:
2025-09-03 11:00:00 +01:00
parent 5deb31cd92
commit d94bf8e033
6 changed files with 581 additions and 126 deletions

View File

@@ -207,4 +207,101 @@ p.cssvalid,p.htmlvalid {float:left;margin-right:20px}
.maindiv {width:100%;overflow-x:auto;font-size:1cqw}
.traffictable {border-collapse:collapse;width:98%}
.divseeinbrowser{text-align:center;}
.bordercollapse{border-collapse:collapse;}
.bordercollapse{border-collapse:collapse;}
/* ==============================================
Summary Logs Section (scoped under .mailstats-summary)
============================================== */
.mailstats-summary .summary-container {
width: 100%;
overflow-x: auto;
font-size: 0.85vw;
}
/* Table styling */
.mailstats-summary .summary-table {
border-collapse: collapse;
width: 98%;
font-size: inherit;
}
.mailstats-summary .summary-table th {
text-align: left;
padding: 0.5em;
border-bottom: 2px solid #ddd;
background-color: #f8f8f8;
}
.mailstats-summary .summary-table td {
padding: 0.5em;
border-bottom: 1px solid #ddd;
word-break: break-word; /* Allows breaking long words at arbitrary points */
overflow-wrap: break-word; /* Modern standard for breaking long words */
hyphens: auto; /* Optionally adds hyphenation if supported */
}
/* Zebra striping */
.mailstats-summary .summary-table tbody tr:nth-child(even) {
background-color: #fafafa;
}
/* Pagination */
.mailstats-summary .pagination {
margin-top: 1em;
}
.mailstats-summary .pagination a {
text-decoration: none;
color: #0066cc;
padding: 0.3em 0.6em;
}
.mailstats-summary .pagination a:hover {
text-decoration: underline;
}
.mailstats-summary table.stripes {
border-collapse: collapse;
width: 95%;
overflow-x: auto;
margin: 0.6% auto;
}
/* Optional zebra striping */
.mailstats-summary table.stripes tbody tr:nth-child(even) {
background-color: #fafafa;
}
/* ==============================================
Log Detail Page (scoped under .mailstats-detail)
============================================== */
.mailstats-detail .detail-container {
width: 100%;
max-width: 1200px;
margin: 1em auto;
padding: 0 1em;
}
/* Preformatted log box */
.mailstats-detail .log {
white-space: pre-wrap;
word-wrap: break-word;
background: #111;
color: #eee;
padding: 1em;
border-radius: 6px;
font-family: monospace, monospace;
font-size: 0.95em;
line-height: 1.4;
overflow-x: auto;
}
/* Back link styling */
.mailstats-detail a {
color: #0066cc;
text-decoration: none;
}
.mailstats-detail a:hover {
text-decoration: underline;
}

View File

@@ -1,51 +1,240 @@
<?php
header('Content-Type: text/plain');
// Security headers
header('Content-Type: text/html; charset=UTF-8');
header("Content-Security-Policy: default-src 'self'; script-src 'none'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; base-uri 'none'; object-src 'none'; frame-ancestors 'none'");
header('X-Content-Type-Options: nosniff');
header('Referrer-Policy: no-referrer');
header('Permissions-Policy: geolocation=(), microphone=(), camera=()');
header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
header('Pragma: no-cache');
if (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') {
header('Strict-Transport-Security: max-age=31536000; includeSubDomains');
}
$input_param = isset($_GET['id']) ? $_GET['id'] : '9999';
function e($s) {
return htmlspecialchars((string)$s, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
// Set the directory and file names
$directory = "/opt/mailstats/logs";
$files = ['current1', 'current2'];
// Configuration: env first, then fallback to optional file
$servername = getenv('MAILSTATS_DB_HOST') ?: 'localhost';
$username = getenv('MAILSTATS_DB_USER') ?: '';
$password = getenv('MAILSTATS_DB_PASS') ?: '';
$dbname = getenv('MAILSTATS_DB_NAME') ?: '';
function process_file($file_path, $input_param) {
$file = fopen($file_path, 'r');
$match = "/ $input_param /";
$endmatch = "/cleaning up after $input_param/";
while (($line = fgets($file)) !== false) {
// Check if the line contains the input_parameter
if (preg_match($match,$line) === 1) {
echo $line;
} elseif (preg_match($endmatch,$line) === 1) {
echo $line;
exit();
if ($username === '' || $password === '' || $dbname === '') {
$cfgPath = '/etc/mailstats/db.php'; // optional fallback config file
if (is_readable($cfgPath)) {
$cfg = include $cfgPath;
$servername = $cfg['host'] ?? $servername;
$username = $cfg['user'] ?? $username;
$password = $cfg['pass'] ?? $password;
$dbname = $cfg['name'] ?? $dbname;
}
}
if ($username === '' || $password === '' || $dbname === '') {
error_log('DB credentials missing (env and config file).');
http_response_code(500);
exit('Service temporarily unavailable.');
}
// Input validation: id
$id = isset($_GET['id']) ? filter_var($_GET['id'], FILTER_VALIDATE_INT) : null;
if ($id === false || $id === null || $id < 1) {
http_response_code(400);
exit('Invalid id');
}
// DB connect with exceptions
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
try {
$conn = new mysqli($servername, $username, $password, $dbname);
$conn->set_charset('utf8mb4');
} catch (mysqli_sql_exception $e) {
error_log('DB connect failed: ' . $e->getMessage());
http_response_code(500);
exit('Service temporarily unavailable.');
}
// Fetch the record and extract PID from JSON logData
try {
$stmt = $conn->prepare('SELECT id, logData FROM SummaryLogs WHERE id = ?');
$stmt->bind_param('i', $id);
$stmt->execute();
$res = $stmt->get_result();
$row = $res->fetch_assoc();
$stmt->close();
} catch (mysqli_sql_exception $e) {
error_log('Query failed: ' . $e->getMessage());
http_response_code(500);
exit('Service temporarily unavailable.');
}
if (!$row) {
http_response_code(404);
exit('Record not found');
}
$logData = $row['logData'];
$pid = null;
$data = json_decode($logData, true, 512, JSON_INVALID_UTF8_SUBSTITUTE);
if (is_array($data)) {
foreach (['id','pid', 'PID', 'Pid', 'process_id', 'ProcessId'] as $k) {
if (isset($data[$k]) && (is_int($data[$k]) || ctype_digit((string)$data[$k]))) {
$pid = (int)$data[$k];
break;
}
}
fclose($file);
}
function tai64nToDate($tai64n) {
// Check if the input TAI64N string is valid
if (preg_match('/^@([0-9a-f]{8})([0-9a-f]{8})$/', $tai64n, $matches)) {
// First part: seconds since epoch
$sec_hex = $matches[1];
// Second part: nanoseconds in hex
$nsec_hex = $matches[2];
if (!$pid || $pid < 1) {
http_response_code(422);
exit('PID not found in this record');
}
// Convert hex to decimal
$seconds = hexdec($sec_hex);
$nanoseconds = hexdec($nsec_hex);
// Calculate the full timestamp in seconds
$timestamp = $seconds + ($nanoseconds / 1e9); // Nanoseconds to seconds
// Format timestamp to 'Y-m-d H:i:s'
return date('Y-m-d H:i:s', $timestamp);
} else {
throw new InvalidArgumentException("Invalid TAI64N format.");
// Journal retrieval using C wrapper
define('FFI_LIB', 'libjournalwrap.so'); // adjust if needed
define('WRAPPER_BIN', '/usr/local/bin/journalwrap'); // fallback executable path
define('MAX_OUTPUT_BYTES', 2_000_000); // 2MB safety cap
function getJournalByPidViaFFI(int $pid): ?string {
if (!extension_loaded('FFI')) {
return null;
}
try {
// Adjust the function signatures to match your wrapper
$ffi = FFI::cdef("
char* journal_get_by_pid(int pid);
void journal_free(char* p);
", FFI_LIB);
$cstr = $ffi->journal_get_by_pid($pid);
if ($cstr === null) {
return '';
}
$out = FFI::string($cstr);
$ffi->journal_free($cstr);
return $out;
} catch (Throwable $e) {
error_log('FFI journal wrapper failed: ' . $e->getMessage());
return null;
}
}
chdir($directory);
foreach ($files as $file) {
process_file($file, $input_param);
function getJournalByPidViaExec(int $pid): ?string {
// Fallback to an external wrapper binary (must be safe and not use shell)
$cmd = WRAPPER_BIN . ' ' . (string)$pid;
$descriptorspec = [
0 => ['pipe', 'r'],
1 => ['pipe', 'w'],
2 => ['pipe', 'w'],
];
$pipes = [];
$proc = proc_open($cmd, $descriptorspec, $pipes, null, null, ['bypass_shell' => true]);
if (!\is_resource($proc)) {
error_log('Failed to start journal wrapper binary');
return null;
}
fclose($pipes[0]); // no stdin
stream_set_blocking($pipes[1], false);
stream_set_blocking($pipes[2], false);
$stdout = '';
$stderr = '';
$start = microtime(true);
$timeout = 10.0; // seconds
$readChunk = 65536;
while (true) {
$status = proc_get_status($proc);
$running = $status['running'];
$read = [$pipes[1], $pipes[2]];
$write = null;
$except = null;
$tv_sec = 0;
$tv_usec = 300000; // 300ms
stream_select($read, $write, $except, $tv_sec, $tv_usec);
foreach ($read as $r) {
if ($r === $pipes[1]) {
$chunk = fread($pipes[1], $readChunk);
if ($chunk !== false && $chunk !== '') {
$stdout .= $chunk;
}
} elseif ($r === $pipes[2]) {
$chunk = fread($pipes[2], $readChunk);
if ($chunk !== false && $chunk !== '') {
$stderr .= $chunk;
}
}
}
if (!$running) {
break;
}
if ((microtime(true) - $start) > $timeout) {
proc_terminate($proc);
$stderr .= "\n[terminated due to timeout]";
break;
}
if (strlen($stdout) + strlen($stderr) > MAX_OUTPUT_BYTES) {
proc_terminate($proc);
$stderr .= "\n[terminated due to output size limit]";
break;
}
}
foreach ($pipes as $p) {
if (is_resource($p)) {
fclose($p);
}
}
$exitCode = proc_close($proc);
if ($exitCode !== 0 && $stderr !== '') {
error_log('journal wrapper stderr: ' . $stderr);
}
return $stdout;
}
$logs = getJournalByPidViaFFI($pid);
if ($logs === null) {
$logs = getJournalByPidViaExec($pid);
}
if ($logs === null) {
http_response_code(500);
exit('Unable to read journal for this PID');
}
// Safety cap to avoid rendering gigantic outputs
if (strlen($logs) > MAX_OUTPUT_BYTES) {
$logs = substr($logs, 0, MAX_OUTPUT_BYTES) . "\n[output truncated]";
}
// Done with DB
$conn->close();
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Log details for PID <?= e($pid) ?> (record <?= e($id) ?>)</title>
<link rel="stylesheet" type="text/css" href="css/mailstats.css" />
</head>
<body>
<div class="mailstats-detail">
<div class="detail-container">
<h1>Log details for PID <?= e($pid) ?> (record <?= e($id) ?>)</h1>
<p><a href="javascript:history.back()">Back</a></p>
<pre class="log"><?= e($logs) ?></pre>
</div>
</div>
</body>
</html>

View File

@@ -172,7 +172,7 @@ function generateLogDataTable($logData) {
$keys = array_keys($mergedData);
$values = array_values($mergedData);
$output = '<table class="stripes" style="border-collapse: collapse; width:95%; overflow-x:auto; margin: 0.6% auto 0.6% auto;"><tbody>';
$output = '<table class="mailstats-summary stripes"><tbody>';
// Divide keys and values into sets of 6
$chunks = array_chunk($keys, 6);
@@ -202,77 +202,78 @@ function generateLogDataTable($logData) {
<link rel="stylesheet" type="text/css" href="css/mailstats.css" />
</head>
<body>
<div style="width:100%; overflow-x:auto; font-size:0.726cqw">
<h1>
Summary Logs for Date: <?= e($date) ?>
<?= $hour === 99 ? ' (All Hours)' : ' at Hour: ' . e($hour) ?>
</h1>
<?php
$startRow = $totalRows > 0 ? ($offset + 1) : 0;
$endRow = min($offset + $limit, $totalRows);
?>
<h3>Found <?= e($totalRows) ?> records. Showing <?= e($startRow) ?><?= e($endRow) ?>.</h3>
<table style="border-collapse:collapse; width:98%">
<thead>
<tr>
<th>Id</th>
<th>Details</th>
<th>Log Data</th>
</tr>
</thead>
<tbody>
<?php if ($result && $result->num_rows > 0): ?>
<?php while ($row = $result->fetch_assoc()): ?>
<?php
$id = (int)$row['id'];
$detailUrl = './ShowDetailedLogs.php?id=' . rawurlencode((string)$id);
?>
<tr>
<td><?= e($id) ?></td>
<td><a href="<?= e($detailUrl) ?>">View details</a></td>
<td><?= generateLogDataTable($row['logData']) ?></td>
</tr>
<?php endwhile; ?>
<?php else: ?>
<div class="mailstats-summary">
<div class="summary-container">
<h1>
Summary Logs for Date: <?= e($date) ?>
<?= $hour === 99 ? ' (All Hours)' : ' at Hour: ' . e($hour) ?>
</h1>
<?php
$startRow = $totalRows > 0 ? ($offset + 1) : 0;
$endRow = min($offset + $limit, $totalRows);
?>
<h3>Found <?= e($totalRows) ?> records. Showing <?= e($startRow) ?><?= e($endRow) ?>.</h3>
<table class="summary-table">
<thead>
<tr>
<td colspan="3">No records found for the specified date and hour.</td>
<th>Id</th>
<th>Details</th>
<th>Log Data</th>
</tr>
</thead>
<tbody>
<?php if ($result && $result->num_rows > 0): ?>
<?php while ($row = $result->fetch_assoc()): ?>
<?php
$id = (int)$row['id'];
$detailUrl = './ShowDetailedLogs.php?id=' . rawurlencode((string)$id);
?>
<tr>
<td><?= e($id) ?></td>
<td><a href="<?= e($detailUrl) ?>">View details</a></td>
<td><?= generateLogDataTable($row['logData']) ?></td>
</tr>
<?php endwhile; ?>
<?php else: ?>
<tr>
<td colspan="3">No records found for the specified date and hour.</td>
</tr>
<?php endif; ?>
</tbody>
</table>
<?php
// Pagination
$baseParams = [
'date' => $date,
'hour' => $hour,
'page_size' => $pageSize
];
$prevPage = $page > 1 ? $page - 1 : null;
$nextPage = ($offset + $limit) < $totalRows ? $page + 1 : null;
?>
<div class="pagination">
<?php if ($prevPage !== null): ?>
<?php
$paramsPrev = $baseParams; $paramsPrev['page'] = $prevPage;
$urlPrev = '?' . http_build_query($paramsPrev, '', '&', PHP_QUERY_RFC3986);
?>
<a href="<?= e($urlPrev) ?>">&laquo; Previous</a>
<?php endif; ?>
</tbody>
</table>
<?php
// Simple pagination links
$baseParams = [
'date' => $date,
'hour' => $hour,
'page_size' => $pageSize
];
$prevPage = $page > 1 ? $page - 1 : null;
$nextPage = ($offset + $limit) < $totalRows ? $page + 1 : null;
?>
<div style="margin-top: 1em;">
<?php if ($prevPage !== null): ?>
<?php
$paramsPrev = $baseParams; $paramsPrev['page'] = $prevPage;
$urlPrev = '?' . http_build_query($paramsPrev, '', '&', PHP_QUERY_RFC3986);
?>
<a href="<?= e($urlPrev) ?>">&laquo; Previous</a>
<?php endif; ?>
<?php if ($nextPage !== null): ?>
<?php
$paramsNext = $baseParams; $paramsNext['page'] = $nextPage;
$urlNext = '?' . http_build_query($paramsNext, '', '&', PHP_QUERY_RFC3986);
?>
<?php if ($prevPage !== null): ?> | <?php endif; ?>
<a href="<?= e($urlNext) ?>">Next &raquo;</a>
<?php endif; ?>
<?php if ($nextPage !== null): ?>
<?php
$paramsNext = $baseParams; $paramsNext['page'] = $nextPage;
$urlNext = '?' . http_build_query($paramsNext, '', '&', PHP_QUERY_RFC3986);
?>
<?php if ($prevPage !== null): ?> | <?php endif; ?>
<a href="<?= e($urlNext) ?>">Next &raquo;</a>
<?php endif; ?>
</div>
</div>
</div>
<?php
// Clean up
if (isset($stmt) && $stmt instanceof mysqli_stmt) { $stmt->close(); }
if (isset($conn) && $conn instanceof mysqli) { $conn->close(); }
?>