268 lines
11 KiB
PHP
268 lines
11 KiB
PHP
#!/usr/bin/env php
|
|
<?php
|
|
require_once __DIR__ . '/../config/config.php';
|
|
|
|
// --- DATABASE CONFIGURATION ---
|
|
const DB_HOST = FRONKDB_DBHOST;
|
|
const DB_NAME = FRONKDB_DBNAME;
|
|
const DB_USER = FRONKDB_DBUSER;
|
|
const DB_PASS = FRONKDB_DBPASS;
|
|
|
|
// --- General & Cache Configuration ---
|
|
const SESSION_CACHE_TTL = 600; // Cache Session ID lookups for 10 minutes
|
|
const ROW_SEPARATOR_INTERVAL = 10; // Draw a divider every 10 rows
|
|
const HEADER_REDRAW_INTERVAL = 30; // Used only in --simple mode
|
|
const IGNORED_EXTENSIONS = ['png','jpg','jpeg','gif','css','js','ico','svg','woff','woff2','json','xml','txt'];
|
|
const LOG_REGEX = '/^(\S+) \S+ \S+ \[(.+?)\] "(GET|POST|PUT|DELETE|HEAD|OPTIONS)\s(.*?)\s\S+" (\d{3}) \S+ "(.*?)" ".*?" "(.*?)"$/';
|
|
|
|
// --- Dynamic Layout Configuration ---
|
|
const FIXED_WIDTH_COLS = ['time' => 10, 'status' => 8, 'method' => 8];
|
|
const FLEX_WIDTH_RATIOS = ['identity' => 0.25, 'req_url' => 0.50, 'ref_view' => 0.25];
|
|
|
|
const COLORS = [
|
|
'green' => "\033[1;32m", 'cyan' => "\033[1;36m", 'yellow' => "\033[1;33m", 'red' => "\033[1;31m",
|
|
'magenta' => "\033[1;35m", 'blue' => "\033[0;34m", 'dim' => "\033[2m", 'reset' => "\033[0m"
|
|
];
|
|
|
|
// --- Global State ---
|
|
$pdo = null;
|
|
$sessionCache = [];
|
|
$logBuffer = [];
|
|
$columnWidths = [];
|
|
$displayableRows = 0;
|
|
$simpleMode = false;
|
|
|
|
// --- Core & Helper Functions ---
|
|
|
|
function get_terminal_dimensions(): array {
|
|
$width = (int)@shell_exec('tput cols');
|
|
$height = (int)@shell_exec('tput lines');
|
|
if ($width > 0 && $height > 0) {
|
|
return ['width' => $width, 'height' => $height];
|
|
}
|
|
// Fallback for environments where tput fails
|
|
return ['width' => 160, 'height' => 40];
|
|
}
|
|
|
|
function calculate_layout_sizes(): void {
|
|
global $columnWidths, $displayableRows;
|
|
$dims = get_terminal_dimensions();
|
|
$terminalWidth = $dims['width'];
|
|
$terminalHeight = $dims['height'];
|
|
|
|
$displayableRows = $terminalHeight - 5;
|
|
$fixedTotalWidth = array_sum(FIXED_WIDTH_COLS);
|
|
$numCols = count(FIXED_WIDTH_COLS) + count(FLEX_WIDTH_RATIOS);
|
|
$borderWidth = $numCols + 1;
|
|
$flexTotalWidth = $terminalWidth - $fixedTotalWidth - $borderWidth;
|
|
|
|
$columnWidths = FIXED_WIDTH_COLS;
|
|
foreach (FLEX_WIDTH_RATIOS as $name => $ratio) {
|
|
$columnWidths[$name] = (int)floor($flexTotalWidth * $ratio);
|
|
}
|
|
}
|
|
|
|
function db_connect(): ?PDO {
|
|
$dsn = "mysql:host=" . DB_HOST . ";dbname=" . DB_NAME . ";charset=utf8mb4";
|
|
try {
|
|
return new PDO($dsn, DB_USER, DB_PASS, [
|
|
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
|
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
|
PDO::ATTR_EMULATE_PREPARES => false,
|
|
]);
|
|
} catch (PDOException $e) { return null; }
|
|
}
|
|
|
|
function getIdentityFromSession(string $sessionId): ?array {
|
|
global $pdo, $sessionCache;
|
|
$now = time();
|
|
if (isset($sessionCache[$sessionId]) && ($now - $sessionCache[$sessionId]['timestamp'] < SESSION_CACHE_TTL)) {
|
|
return $sessionCache[$sessionId]['data'];
|
|
}
|
|
try {
|
|
$stmt = $pdo->prepare("SELECT w.name, a.company FROM Worker w LEFT JOIN Address a ON w.address_id = a.id WHERE w.sessionid = ? LIMIT 1");
|
|
$stmt->execute([$sessionId]);
|
|
$result = $stmt->fetch();
|
|
$sessionCache[$sessionId] = ['data' => $result ?: null, 'timestamp' => $now];
|
|
return $result ?: null;
|
|
} catch (PDOException $e) { return null; }
|
|
}
|
|
|
|
function getControllerFromUrl(string $url): string {
|
|
if ($url === '[direct]' || $url === '-') return '[direct]';
|
|
$path = parse_url($url, PHP_URL_PATH);
|
|
if (empty($path) || $path === '/') return '[root]';
|
|
$parts = explode('/', trim($path, '/'));
|
|
return ucfirst($parts[0]);
|
|
}
|
|
|
|
function getStatusColor(int $statusCode): string {
|
|
if ($statusCode >= 500) return 'red';
|
|
if ($statusCode >= 400) return 'yellow';
|
|
if ($statusCode >= 300) return 'cyan';
|
|
return 'green';
|
|
}
|
|
|
|
function parseLogLine(string $line): ?array {
|
|
if (!preg_match(LOG_REGEX, $line, $matches)) return null;
|
|
$url = $matches[4];
|
|
$ext = strtolower(pathinfo(parse_url($url, PHP_URL_PATH), PATHINFO_EXTENSION));
|
|
if (in_array($ext, IGNORED_EXTENSIONS, true)) return null;
|
|
$ts = DateTime::createFromFormat('d/M/Y:H:i:s O', $matches[2]);
|
|
return ["ip" => $matches[1], "timestamp" => $ts ? $ts->format('H:i:s') : '??:??:??', "method" => $matches[3],
|
|
"url" => $url, "status" => (int)$matches[5], "referrer" => $matches[6], "sessionid" => $matches[7]];
|
|
}
|
|
|
|
// --- UI Drawing Functions ---
|
|
|
|
function draw_divider(string $left, string $mid, string $right): void {
|
|
global $columnWidths;
|
|
echo COLORS['dim'];
|
|
$cols = ['identity', 'time', 'status', 'method', 'req_url', 'ref_view'];
|
|
echo $left;
|
|
foreach ($cols as $i => $key) {
|
|
echo str_repeat('─', $columnWidths[$key]);
|
|
echo ($i < count($cols) - 1) ? $mid : '';
|
|
}
|
|
echo $right . PHP_EOL . COLORS['reset'];
|
|
}
|
|
|
|
/**
|
|
* A fully multi-byte aware function to format cell content with padding, truncation, and color.
|
|
*/
|
|
function format_cell(string $text, int $width, string $color = ''): string {
|
|
$text = ' ' . $text; // Add leading space
|
|
// mb_strimwidth is width-aware, not just character count-aware
|
|
$visible_width = mb_strwidth($text, 'UTF-8');
|
|
|
|
if ($visible_width > $width) {
|
|
// Truncate and add "..." suffix
|
|
$text = mb_strimwidth($text, 0, $width - 4, ' ...', 'UTF-8');
|
|
}
|
|
|
|
// Manual padding because str_pad is not multi-byte safe
|
|
$final_width = mb_strwidth($text, 'UTF-8');
|
|
$padding = $width - $final_width;
|
|
$padded_text = $text . str_repeat(' ', $padding > 0 ? $padding : 0);
|
|
|
|
return ($color ? COLORS[$color] : '') . $padded_text . ($color ? COLORS['reset'] : '');
|
|
}
|
|
|
|
function draw_header(string $logFile, bool $isRedraw = false): void {
|
|
global $simpleMode;
|
|
if (!$simpleMode && !$isRedraw) {
|
|
// Only clear screen in normal mode on initial draw
|
|
echo "\033[H\033[J";
|
|
}
|
|
$title = basename($logFile);
|
|
$mode_indicator = $simpleMode ? " [Compatibility Mode]" : "";
|
|
echo ($isRedraw ? PHP_EOL : '') . COLORS['magenta'] . " watching {$title}{$mode_indicator} | Press Ctrl+C to exit" . COLORS['reset'] . PHP_EOL;
|
|
draw_divider('┌', '┬', '┐');
|
|
$headers = [' IDENTITY', ' TIME', ' STATUS', ' METHOD', ' REQUEST URL', ' FROM VIEW'];
|
|
$keys = ['identity', 'time', 'status', 'method', 'req_url', 'ref_view'];
|
|
echo COLORS['dim'] . '│' . COLORS['magenta'];
|
|
foreach ($keys as $i => $key) {
|
|
echo str_pad($headers[$i], $GLOBALS['columnWidths'][$key]) . COLORS['dim'] . '│' . COLORS['magenta'];
|
|
}
|
|
echo COLORS['reset'] . PHP_EOL;
|
|
draw_divider('├', '┼', '┤');
|
|
}
|
|
|
|
function redraw_screen(string $logFile): void {
|
|
global $logBuffer;
|
|
echo "\033[H\033[J"; // Move cursor to top left and clear the screen
|
|
draw_header($logFile, true);
|
|
|
|
foreach ($logBuffer as $index => $data) {
|
|
echo COLORS['dim'] . '│';
|
|
echo format_cell($data['identity'], $GLOBALS['columnWidths']['identity'], 'yellow');
|
|
echo COLORS['dim'] . '│';
|
|
echo format_cell($data['timestamp'], $GLOBALS['columnWidths']['time']);
|
|
echo COLORS['dim'] . '│';
|
|
echo format_cell($data['status'], $GLOBALS['columnWidths']['status'], getStatusColor($data['status']));
|
|
echo COLORS['dim'] . '│';
|
|
echo format_cell($data['method'], $GLOBALS['columnWidths']['method']);
|
|
echo COLORS['dim'] . '│';
|
|
echo format_cell($data['url'], $GLOBALS['columnWidths']['req_url'], 'green');
|
|
echo COLORS['dim'] . '│';
|
|
echo format_cell($data['referrer_view'], $GLOBALS['columnWidths']['ref_view'], 'blue');
|
|
echo COLORS['dim'] . '│' . PHP_EOL;
|
|
|
|
if (($index + 1) % ROW_SEPARATOR_INTERVAL === 0 && ($index + 1) < count($logBuffer)) {
|
|
draw_divider('├', '┼', '┤');
|
|
}
|
|
}
|
|
draw_divider('└', '┴', '┘');
|
|
}
|
|
|
|
// --- Main Application ---
|
|
if (!extension_loaded('mbstring')) { die("Error: The 'mbstring' PHP extension is required. Please install it (e.g., sudo apt install php-mbstring).\n"); }
|
|
if (function_exists('pcntl_signal')) { pcntl_async_signals(true); pcntl_signal(SIGINT, function () { echo "\nWatcher stopped.\n"; exit; }); }
|
|
|
|
// Check for --simple compatibility mode flag
|
|
$simpleMode = in_array('--simple', $argv);
|
|
|
|
calculate_layout_sizes();
|
|
$logFile = $argv[1] ?? '/var/log/apache2/access.log';
|
|
if ($logFile === '--simple') $logFile = '/var/log/apache2/access.log';
|
|
if (!is_readable($logFile)) { die("Error: Log file not readable: $logFile\n"); }
|
|
|
|
$pdo = db_connect();
|
|
if (!$pdo) { exit(1); }
|
|
|
|
$handle = fopen($logFile, 'r');
|
|
$position = filesize($logFile);
|
|
fseek($handle, $position);
|
|
|
|
$lineCounter = 0;
|
|
draw_header($logFile);
|
|
|
|
while (true) {
|
|
clearstatcache(true, $logFile);
|
|
$currentSize = filesize($logFile);
|
|
|
|
if ($currentSize < $position) { fseek($handle, 0); $position = 0; $logBuffer = []; }
|
|
if ($currentSize > $position) {
|
|
fseek($handle, $position);
|
|
$newContent = fread($handle, $currentSize - $position);
|
|
$position = $currentSize;
|
|
$hasNewData = false;
|
|
|
|
foreach (explode(PHP_EOL, trim($newContent)) as $line) {
|
|
if (empty($line)) continue;
|
|
$data = parseLogLine($line);
|
|
if ($data === null || $data['sessionid'] === '-' || $data['method'] === 'OPTIONS') continue;
|
|
$identity = getIdentityFromSession($data['sessionid']);
|
|
if ($identity === null) continue;
|
|
|
|
$hasNewData = true;
|
|
$data['identity'] = $identity['name'] . (!empty($identity['company']) ? " ({$identity['company']})" : '');
|
|
$data['referrer_view'] = getControllerFromUrl($data['referrer']);
|
|
|
|
if ($simpleMode) {
|
|
// Simple mode: just print the new row
|
|
echo COLORS['dim'] . '│';
|
|
echo format_cell($data['identity'], $GLOBALS['columnWidths']['identity'], 'yellow');
|
|
echo COLORS['dim'] . '│' . format_cell($data['timestamp'], $GLOBALS['columnWidths']['time']);
|
|
echo COLORS['dim'] . '│' . format_cell($data['status'], $GLOBALS['columnWidths']['status'], getStatusColor($data['status']));
|
|
echo COLORS['dim'] . '│' . format_cell($data['method'], $GLOBALS['columnWidths']['method']);
|
|
echo COLORS['dim'] . '│' . format_cell($data['url'], $GLOBALS['columnWidths']['req_url'], 'green');
|
|
echo COLORS['dim'] . '│' . format_cell($data['referrer_view'], $GLOBALS['columnWidths']['ref_view'], 'blue');
|
|
echo COLORS['dim'] . '│' . PHP_EOL;
|
|
|
|
$lineCounter++;
|
|
if ($lineCounter >= HEADER_REDRAW_INTERVAL) { draw_header($logFile, true); $lineCounter = 0; }
|
|
} else {
|
|
// Normal mode: add to buffer for redrawing
|
|
$logBuffer[] = $data;
|
|
if (count($logBuffer) > $displayableRows) { array_shift($logBuffer); }
|
|
}
|
|
}
|
|
if ($hasNewData && !$simpleMode) {
|
|
redraw_screen($logFile);
|
|
}
|
|
}
|
|
|
|
usleep(250000); // 0.25 seconds
|
|
}
|