Get detail logs page working - WIP
This commit is contained in:
179
journalwrap.c
Normal file
179
journalwrap.c
Normal file
@@ -0,0 +1,179 @@
|
||||
#include <systemd/sd-journal.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdint.h>
|
||||
#include <errno.h>
|
||||
#include <time.h>
|
||||
|
||||
#ifndef MAX_OUTPUT_BYTES
|
||||
#define MAX_OUTPUT_BYTES (2 * 1000 * 1000) // 2 MB
|
||||
#endif
|
||||
|
||||
static int append_bytes(char **buf, size_t *len, size_t *cap, const char *src, size_t n) {
|
||||
if (*len + n + 1 > *cap) {
|
||||
size_t newcap = (*cap == 0) ? 8192 : *cap;
|
||||
while (*len + n + 1 > newcap) {
|
||||
newcap *= 2;
|
||||
if (newcap > (size_t)(MAX_OUTPUT_BYTES + 65536)) {
|
||||
newcap = (size_t)(MAX_OUTPUT_BYTES + 65536);
|
||||
break;
|
||||
}
|
||||
}
|
||||
char *nbuf = realloc(*buf, newcap);
|
||||
if (!nbuf) return -1;
|
||||
*buf = nbuf; *cap = newcap;
|
||||
}
|
||||
memcpy(*buf + *len, src, n);
|
||||
*len += n;
|
||||
(*buf)[*len] = '\0';
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int append_cstr(char **buf, size_t *len, size_t *cap, const char *s) {
|
||||
return append_bytes(buf, len, cap, s, strlen(s));
|
||||
}
|
||||
|
||||
static size_t min_size(size_t a, size_t b) { return a < b ? a : b; }
|
||||
|
||||
static void sanitize_text(char *s, size_t n) {
|
||||
for (size_t i = 0; i < n; i++) if (s[i] == '\0') s[i] = ' ';
|
||||
}
|
||||
|
||||
static void format_ts(char *out, size_t outsz, uint64_t usec) {
|
||||
time_t sec = (time_t)(usec / 1000000ULL);
|
||||
struct tm tm;
|
||||
localtime_r(&sec, &tm);
|
||||
strftime(out, outsz, "%Y-%m-%d %H:%M:%S", &tm);
|
||||
}
|
||||
|
||||
static const char* field_value(const void *data, size_t len, const char *key, size_t *vlen) {
|
||||
size_t klen = strlen(key);
|
||||
if (len < klen + 1) return NULL;
|
||||
const char *p = (const char *)data;
|
||||
if (memcmp(p, key, klen) != 0 || p[klen] != '=') return NULL;
|
||||
*vlen = len - (klen + 1);
|
||||
return p + klen + 1;
|
||||
}
|
||||
|
||||
static int append_entry_line(sd_journal *j, char **buf, size_t *len, size_t *cap) {
|
||||
uint64_t usec = 0;
|
||||
(void)sd_journal_get_realtime_usec(j, &usec);
|
||||
char ts[32];
|
||||
format_ts(ts, sizeof(ts), usec);
|
||||
|
||||
const void *data = NULL;
|
||||
size_t dlen = 0;
|
||||
const char *message = NULL;
|
||||
size_t mlen = 0;
|
||||
|
||||
int r = sd_journal_get_data(j, "MESSAGE", &data, &dlen);
|
||||
if (r >= 0) message = field_value(data, dlen, "MESSAGE", &mlen);
|
||||
|
||||
const char *ident = NULL;
|
||||
size_t ilen = 0;
|
||||
r = sd_journal_get_data(j, "SYSLOG_IDENTIFIER", &data, &dlen);
|
||||
if (r >= 0) {
|
||||
ident = field_value(data, dlen, "SYSLOG_IDENTIFIER", &ilen);
|
||||
} else if (sd_journal_get_data(j, "_COMM", &data, &dlen) >= 0) {
|
||||
ident = field_value(data, dlen, "_COMM", &ilen);
|
||||
}
|
||||
|
||||
if (append_cstr(buf, len, cap, "[") < 0) return -1;
|
||||
if (append_cstr(buf, len, cap, ts) < 0) return -1;
|
||||
if (append_cstr(buf, len, cap, "] ") < 0) return -1;
|
||||
if (ident && ilen > 0) {
|
||||
if (append_bytes(buf, len, cap, ident, ilen) < 0) return -1;
|
||||
if (append_cstr(buf, len, cap, ": ") < 0) return -1;
|
||||
}
|
||||
|
||||
if (message && mlen > 0) {
|
||||
char *tmp = malloc(mlen);
|
||||
if (!tmp) return -1;
|
||||
memcpy(tmp, message, mlen);
|
||||
sanitize_text(tmp, mlen);
|
||||
size_t to_copy = min_size(mlen, (size_t)(MAX_OUTPUT_BYTES > *len ? MAX_OUTPUT_BYTES - *len : 0));
|
||||
int ok = append_bytes(buf, len, cap, tmp, to_copy);
|
||||
free(tmp);
|
||||
if (ok < 0) return -1;
|
||||
} else {
|
||||
const char *keys[] = {"PRIORITY","SYSLOG_IDENTIFIER","_COMM","_EXE","_CMDLINE","MESSAGE"};
|
||||
for (size_t i = 0; i < sizeof(keys)/sizeof(keys[0]); i++) {
|
||||
if (sd_journal_get_data(j, keys[i], &data, &dlen) < 0) continue;
|
||||
if (append_cstr(buf, len, cap, (i == 0 ? "" : " ")) < 0) return -1;
|
||||
if (append_bytes(buf, len, cap, (const char*)data, min_size(dlen, (size_t)(MAX_OUTPUT_BYTES - *len))) < 0) return -1;
|
||||
}
|
||||
}
|
||||
|
||||
if (*len < MAX_OUTPUT_BYTES) {
|
||||
if (append_cstr(buf, len, cap, "\n") < 0) return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static char* journal_get_by_pid_impl(int pid) {
|
||||
if (pid <= 0) { char *z = malloc(1); if (z) z[0] = '\0'; return z; }
|
||||
|
||||
sd_journal *j = NULL;
|
||||
if (sd_journal_open(&j, SD_JOURNAL_LOCAL_ONLY) < 0) {
|
||||
char *z = malloc(1); if (z) z[0] = '\0'; return z;
|
||||
}
|
||||
|
||||
char match[64];
|
||||
snprintf(match, sizeof(match), "_PID=%d", pid);
|
||||
if (sd_journal_add_match(j, match, 0) < 0) {
|
||||
sd_journal_close(j);
|
||||
char *z = malloc(1); if (z) z[0] = '\0'; return z;
|
||||
}
|
||||
|
||||
sd_journal_seek_head(j);
|
||||
|
||||
char *buf = NULL; size_t len = 0, cap = 0;
|
||||
int r;
|
||||
while ((r = sd_journal_next(j)) > 0) {
|
||||
if (len >= MAX_OUTPUT_BYTES) break;
|
||||
if (append_entry_line(j, &buf, &len, &cap) < 0) {
|
||||
free(buf); sd_journal_close(j); return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
if (len >= MAX_OUTPUT_BYTES) {
|
||||
const char *trunc = "[output truncated]\n";
|
||||
(void)append_bytes(&buf, &len, &cap, trunc, strlen(trunc));
|
||||
}
|
||||
|
||||
if (!buf) { buf = malloc(1); if (!buf) { sd_journal_close(j); return NULL; } buf[0] = '\0'; }
|
||||
sd_journal_close(j);
|
||||
return buf;
|
||||
}
|
||||
|
||||
#ifdef __GNUC__
|
||||
__attribute__((visibility("default")))
|
||||
#endif
|
||||
char* journal_get_by_pid(int pid) { return journal_get_by_pid_impl(pid); }
|
||||
|
||||
#ifdef __GNUC__
|
||||
__attribute__((visibility("default")))
|
||||
#endif
|
||||
void journal_free(char* p) { free(p); }
|
||||
|
||||
#ifdef BUILD_CLI
|
||||
static int parse_pid(const char *s, int *out) {
|
||||
if (!s || !*s) return -1;
|
||||
char *end = NULL;
|
||||
errno = 0;
|
||||
long v = strtol(s, &end, 10);
|
||||
if (errno != 0 || end == s || *end != '\0' || v <= 0 || v > 0x7fffffffL) return -1;
|
||||
*out = (int)v; return 0;
|
||||
}
|
||||
int main(int argc, char **argv) {
|
||||
if (argc != 2) { fprintf(stderr, "Usage: %s <pid>\n", argv[0]); return 2; }
|
||||
int pid = 0;
|
||||
if (parse_pid(argv[1], &pid) != 0) { fprintf(stderr, "Invalid pid\n"); return 2; }
|
||||
char *out = journal_get_by_pid_impl(pid);
|
||||
if (!out) { fprintf(stderr, "Out of memory or error\n"); return 1; }
|
||||
fputs(out, stdout);
|
||||
free(out);
|
||||
return 0;
|
||||
}
|
||||
#endif
|
@@ -208,3 +208,100 @@ p.cssvalid,p.htmlvalid {float:left;margin-right:20px}
|
||||
.traffictable {border-collapse:collapse;width:98%}
|
||||
.divseeinbrowser{text-align:center;}
|
||||
.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;
|
||||
}
|
@@ -1,51 +1,240 @@
|
||||
<?php
|
||||
header('Content-Type: text/plain');
|
||||
|
||||
$input_param = isset($_GET['id']) ? $_GET['id'] : '9999';
|
||||
|
||||
// Set the directory and file names
|
||||
$directory = "/opt/mailstats/logs";
|
||||
$files = ['current1', 'current2'];
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
fclose($file);
|
||||
// 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');
|
||||
}
|
||||
|
||||
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];
|
||||
function e($s) {
|
||||
return htmlspecialchars((string)$s, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
|
||||
}
|
||||
|
||||
// Convert hex to decimal
|
||||
$seconds = hexdec($sec_hex);
|
||||
$nanoseconds = hexdec($nsec_hex);
|
||||
// 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') ?: '';
|
||||
|
||||
// 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.");
|
||||
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;
|
||||
}
|
||||
}
|
||||
chdir($directory);
|
||||
foreach ($files as $file) {
|
||||
process_file($file, $input_param);
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!$pid || $pid < 1) {
|
||||
http_response_code(422);
|
||||
exit('PID not found in this record');
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
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>
|
@@ -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,7 +202,8 @@ 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">
|
||||
<div class="mailstats-summary">
|
||||
<div class="summary-container">
|
||||
<h1>
|
||||
Summary Logs for Date: <?= e($date) ?>
|
||||
<?= $hour === 99 ? ' (All Hours)' : ' at Hour: ' . e($hour) ?>
|
||||
@@ -213,7 +214,7 @@ function generateLogDataTable($logData) {
|
||||
?>
|
||||
<h3>Found <?= e($totalRows) ?> records. Showing <?= e($startRow) ?>–<?= e($endRow) ?>.</h3>
|
||||
|
||||
<table style="border-collapse:collapse; width:98%">
|
||||
<table class="summary-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Id</th>
|
||||
@@ -243,7 +244,7 @@ function generateLogDataTable($logData) {
|
||||
</table>
|
||||
|
||||
<?php
|
||||
// Simple pagination links
|
||||
// Pagination
|
||||
$baseParams = [
|
||||
'date' => $date,
|
||||
'hour' => $hour,
|
||||
@@ -252,7 +253,7 @@ function generateLogDataTable($logData) {
|
||||
$prevPage = $page > 1 ? $page - 1 : null;
|
||||
$nextPage = ($offset + $limit) < $totalRows ? $page + 1 : null;
|
||||
?>
|
||||
<div style="margin-top: 1em;">
|
||||
<div class="pagination">
|
||||
<?php if ($prevPage !== null): ?>
|
||||
<?php
|
||||
$paramsPrev = $baseParams; $paramsPrev['page'] = $prevPage;
|
||||
@@ -260,7 +261,6 @@ function generateLogDataTable($logData) {
|
||||
?>
|
||||
<a href="<?= e($urlPrev) ?>">« Previous</a>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($nextPage !== null): ?>
|
||||
<?php
|
||||
$paramsNext = $baseParams; $paramsNext['page'] = $nextPage;
|
||||
@@ -271,8 +271,9 @@ function generateLogDataTable($logData) {
|
||||
<?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(); }
|
||||
?>
|
||||
|
@@ -1,17 +0,0 @@
|
||||
#!/bin/bash
|
||||
#exec 1> >(logger -t $(basename $0)) 2>&1
|
||||
perl /usr/bin/mailstats.pl /var/log/qpsmtpd/\@* /var/log/qpsmtpd/current /var/log/sqpsmtpd/\@* /var/log/sqpsmtpd/current
|
||||
# and run new python one - start by copying and decoding log files
|
||||
yesterday_date=$(date -d "yesterday" +'%mm %d')
|
||||
#cd /var/log/qpsmtpd
|
||||
#cat \@* current >/opt/mailstats/logs/current1 2>/dev/null
|
||||
#cd /var/log/sqpsmtpd
|
||||
#cat \@* current >/opt/mailstats/logs/current2 2>/dev/null
|
||||
cd /opt/mailstats/logs
|
||||
#cat current1 current2 2>/dev/null | /usr/local/bin/tai64nlocal | grep "$yesterday_date" > current1.log
|
||||
python3 /usr/bin/mailstats-convert-log-sme10-to-sme11.py
|
||||
yesterday_date=$(date -d "yesterday" +'%b %d')
|
||||
cat output_log.txt | grep "$yesterday_date" | sort >current.log
|
||||
ls -l
|
||||
python3 /usr/bin/mailstats.py
|
||||
echo "Done"
|
@@ -32,12 +32,17 @@ AutoReqProv: no
|
||||
|
||||
%description
|
||||
A script that via cron.d e-mails mail statistics to admin on a daily basis.
|
||||
See http://www.contribs.org/bugzilla/show_bug.cgi?id=819
|
||||
See https://wiki.koozali.org/mailstats
|
||||
|
||||
%changelog
|
||||
* Tue Sep 02 2025 Brian Read <brianr@koozali.org> 11.1-5.sme
|
||||
- Speed up Journal access [SME: 13121]
|
||||
- Fix missing blacklist URL [SME: 13121]
|
||||
- Add extra security to php show summary page [SME: 13121]
|
||||
- Fix up CSS for Summary Page [SME: 13121]
|
||||
- Get Detail logs page working and prettyfy [SME: 13121]
|
||||
- Add in C wrapper source code to interrogate journal [SME: 13121]
|
||||
|
||||
|
||||
|
||||
* Mon Sep 01 2025 Brian Read <brianr@koozali.org> 11.1-4.sme
|
||||
@@ -133,6 +138,8 @@ perl createlinks
|
||||
/bin/rm -rf $RPM_BUILD_ROOT
|
||||
(cd root ; /usr/bin/find . -depth -print | /bin/cpio -dump $RPM_BUILD_ROOT)
|
||||
chmod +x $RPM_BUILD_ROOT/usr/bin/runmailstats.sh
|
||||
chown root:root $RPM_BUILD_ROOT/etc/mailstats/dp.php
|
||||
chmod 0600 $RPM_BUILD_ROOT/etc/mailstats/db.php
|
||||
# Define the placeholder and generate the current date and time
|
||||
now=$(date +"%Y-%m-%d %H:%M:%S")
|
||||
|
||||
@@ -146,7 +153,6 @@ sed -i "s|__BUILD_DATE_TIME__|$now|" $RPM_BUILD_ROOT/usr/bin/mailstats.py
|
||||
/usr/bin/pip3 install -q pymysql
|
||||
/usr/bin/pip3 install -q numpy
|
||||
/usr/bin/pip3 install -q pandas
|
||||
/usr/bin/pip3 install -q plotly
|
||||
|
||||
%clean
|
||||
/bin/rm -rf $RPM_BUILD_ROOT
|
||||
|
Reference in New Issue
Block a user