mirror of
https://github.com/NNTmux/newznab-tmux.git
synced 2026-08-29 01:08:56 +00:00
Make redis monitor a service
This commit is contained in:
@@ -0,0 +1,146 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Services\RedisMonitorService;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class RedisMonitor extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'redis:monitor
|
||||
{--connection=default : Redis connection name from config/database.php}
|
||||
{--refresh=30 : Refresh interval in seconds}
|
||||
{--once : Display stats once and exit}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Monitor Redis server with a visual dashboard';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle(): int
|
||||
{
|
||||
$connection = $this->option('connection');
|
||||
$refresh = (int) $this->option('refresh');
|
||||
$once = $this->option('once');
|
||||
|
||||
try {
|
||||
$service = new RedisMonitorService($connection, $refresh);
|
||||
|
||||
if ($once) {
|
||||
$stats = $service->getStats();
|
||||
|
||||
if (!$stats['connected']) {
|
||||
$this->error("Cannot connect to Redis at {$stats['host']}:{$stats['port']}");
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
$this->displayStats($stats);
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
// Start the continuous monitor
|
||||
$service->monitor();
|
||||
|
||||
return Command::SUCCESS;
|
||||
|
||||
} catch (\Exception $e) {
|
||||
$this->error('Redis Monitor failed: ' . $e->getMessage());
|
||||
return Command::FAILURE;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Display stats in table format (for --once mode).
|
||||
*/
|
||||
protected function displayStats(array $stats): void
|
||||
{
|
||||
$this->newLine();
|
||||
$this->line("🔴 Redis Monitor - {$stats['host']}:{$stats['port']} (v{$stats['version']})");
|
||||
$this->newLine();
|
||||
|
||||
$this->line('📊 Server Status');
|
||||
$this->table(
|
||||
['Metric', 'Value'],
|
||||
[
|
||||
['Status', '● ONLINE'],
|
||||
['Role', $stats['role']],
|
||||
['Uptime', "{$stats['uptime_days']} days"],
|
||||
]
|
||||
);
|
||||
|
||||
$this->newLine();
|
||||
$this->line('💾 Memory Usage');
|
||||
$this->table(
|
||||
['Metric', 'Value'],
|
||||
[
|
||||
['Used Memory', $this->formatBytes($stats['used_memory'])],
|
||||
['Peak Memory', $this->formatBytes($stats['used_memory_peak'])],
|
||||
['RSS Memory', $this->formatBytes($stats['used_memory_rss'])],
|
||||
['Fragmentation Ratio', $stats['mem_fragmentation_ratio']],
|
||||
]
|
||||
);
|
||||
|
||||
$this->newLine();
|
||||
$this->line('👥 Clients & Connections');
|
||||
$this->table(
|
||||
['Metric', 'Value'],
|
||||
[
|
||||
['Connected Clients', number_format($stats['connected_clients'])],
|
||||
['Blocked Clients', number_format($stats['blocked_clients'])],
|
||||
['Total Connections', number_format($stats['total_connections'])],
|
||||
]
|
||||
);
|
||||
|
||||
$this->newLine();
|
||||
$this->line('⚡ Performance Metrics');
|
||||
$this->table(
|
||||
['Metric', 'Value'],
|
||||
[
|
||||
['Commands Processed', number_format($stats['total_commands'])],
|
||||
['Operations/sec', number_format($stats['ops_per_sec']) . ' ops/s'],
|
||||
['Network Input', $stats['input_kbps'] . ' KB/s'],
|
||||
['Network Output', $stats['output_kbps'] . ' KB/s'],
|
||||
]
|
||||
);
|
||||
|
||||
$this->newLine();
|
||||
$this->line('🔑 Keyspace Statistics');
|
||||
$this->table(
|
||||
['Metric', 'Value'],
|
||||
[
|
||||
['Total Keys (db0)', number_format($stats['db_keys'])],
|
||||
['Keyspace Hits', number_format($stats['keyspace_hits'])],
|
||||
['Keyspace Misses', number_format($stats['keyspace_misses'])],
|
||||
['Hit Rate', $stats['hit_rate'] . '%'],
|
||||
['Expired Keys', number_format($stats['expired_keys'])],
|
||||
['Evicted Keys', number_format($stats['evicted_keys'])],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format bytes into human-readable string.
|
||||
*/
|
||||
protected function formatBytes(int $bytes): string
|
||||
{
|
||||
if ($bytes >= 1073741824) {
|
||||
return sprintf("%.2f GB", $bytes / 1073741824);
|
||||
} elseif ($bytes >= 1048576) {
|
||||
return sprintf("%.2f MB", $bytes / 1048576);
|
||||
} elseif ($bytes >= 1024) {
|
||||
return sprintf("%.2f KB", $bytes / 1024);
|
||||
}
|
||||
return $bytes . " B";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,524 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use Illuminate\Support\Facades\Redis;
|
||||
|
||||
/**
|
||||
* Service for monitoring Redis server status and performance metrics.
|
||||
*
|
||||
* Provides a modern visual dashboard displaying Redis statistics including
|
||||
* memory usage, client connections, performance metrics, and keyspace data.
|
||||
*/
|
||||
class RedisMonitorService
|
||||
{
|
||||
// ANSI color codes
|
||||
private const RED = "\033[0;31m";
|
||||
private const GREEN = "\033[0;32m";
|
||||
private const YELLOW = "\033[1;33m";
|
||||
private const BLUE = "\033[0;34m";
|
||||
private const MAGENTA = "\033[0;35m";
|
||||
private const CYAN = "\033[0;36m";
|
||||
private const WHITE = "\033[1;37m";
|
||||
private const GRAY = "\033[0;90m";
|
||||
private const BOLD = "\033[1m";
|
||||
private const DIM = "\033[2m";
|
||||
private const NC = "\033[0m"; // No Color
|
||||
|
||||
// Box drawing characters
|
||||
private const H_LINE = "─";
|
||||
private const V_LINE = "│";
|
||||
private const TL_CORNER = "┌";
|
||||
private const TR_CORNER = "┐";
|
||||
private const BL_CORNER = "└";
|
||||
private const BR_CORNER = "┘";
|
||||
private const T_RIGHT = "├";
|
||||
private const T_LEFT = "┤";
|
||||
|
||||
private const CLEAR_EOL = "\033[K";
|
||||
|
||||
protected string $host;
|
||||
protected int $port;
|
||||
protected int $refreshInterval;
|
||||
protected ?string $connection;
|
||||
protected int $termWidth;
|
||||
protected int $boxWidth;
|
||||
protected bool $running = true;
|
||||
|
||||
/**
|
||||
* Create a new RedisMonitorService instance.
|
||||
*
|
||||
* @param string|null $connection Redis connection name (from config/database.php)
|
||||
* @param int $refreshInterval Refresh interval in seconds
|
||||
*/
|
||||
public function __construct(?string $connection = 'default', int $refreshInterval = 30)
|
||||
{
|
||||
$this->connection = $connection;
|
||||
$this->refreshInterval = $refreshInterval;
|
||||
|
||||
// Get connection config
|
||||
$config = config("database.redis.{$connection}", config('database.redis.default'));
|
||||
$this->host = $config['host'] ?? '127.0.0.1';
|
||||
$this->port = (int) ($config['port'] ?? 6379);
|
||||
|
||||
// Get terminal width
|
||||
$this->termWidth = $this->getTerminalWidth();
|
||||
$this->boxWidth = min($this->termWidth - 2, 100); // Cap at 100 for readability
|
||||
}
|
||||
|
||||
/**
|
||||
* Get terminal width.
|
||||
*/
|
||||
protected function getTerminalWidth(): int
|
||||
{
|
||||
$width = 80;
|
||||
if (function_exists('exec')) {
|
||||
$output = [];
|
||||
@exec('tput cols 2>/dev/null', $output);
|
||||
if (!empty($output[0]) && is_numeric($output[0])) {
|
||||
$width = (int) $output[0];
|
||||
}
|
||||
}
|
||||
return max($width, 60);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Redis INFO data.
|
||||
*
|
||||
* @return array|null
|
||||
*/
|
||||
public function getRedisInfo(): ?array
|
||||
{
|
||||
try {
|
||||
return Redis::connection($this->connection)->info();
|
||||
} catch (\Exception $e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the monitor loop.
|
||||
*
|
||||
* @param callable|null $shouldContinue Optional callback to control loop continuation
|
||||
*/
|
||||
public function monitor(?callable $shouldContinue = null): void
|
||||
{
|
||||
// Disable output buffering for immediate display
|
||||
while (ob_get_level()) {
|
||||
ob_end_flush();
|
||||
}
|
||||
|
||||
// Hide cursor
|
||||
$this->hideCursor();
|
||||
|
||||
// Setup cleanup handler
|
||||
$cleanup = function () {
|
||||
$this->showCursor();
|
||||
echo self::NC; // Reset colors
|
||||
};
|
||||
|
||||
// Register shutdown handler
|
||||
register_shutdown_function($cleanup);
|
||||
|
||||
// Handle signals if pcntl is available
|
||||
if (function_exists('pcntl_async_signals')) {
|
||||
pcntl_async_signals(true);
|
||||
}
|
||||
|
||||
if (function_exists('pcntl_signal')) {
|
||||
pcntl_signal(SIGINT, function () use ($cleanup) {
|
||||
$this->running = false;
|
||||
$cleanup();
|
||||
exit(0);
|
||||
});
|
||||
pcntl_signal(SIGTERM, function () use ($cleanup) {
|
||||
$this->running = false;
|
||||
$cleanup();
|
||||
exit(0);
|
||||
});
|
||||
}
|
||||
|
||||
// Initial clear
|
||||
$this->clearScreen();
|
||||
|
||||
while ($this->running) {
|
||||
// Check if we should continue
|
||||
if ($shouldContinue !== null && !$shouldContinue()) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Process signals if available
|
||||
if (function_exists('pcntl_signal_dispatch')) {
|
||||
pcntl_signal_dispatch();
|
||||
}
|
||||
|
||||
// Move cursor to top-left (prevents flicker, better than clearing)
|
||||
$this->moveToTop();
|
||||
|
||||
// Get Redis info
|
||||
$info = $this->getRedisInfo();
|
||||
|
||||
if ($info === null) {
|
||||
$this->displayConnectionError();
|
||||
} else {
|
||||
$this->displayDashboard($info);
|
||||
}
|
||||
|
||||
// Flush output immediately
|
||||
if (function_exists('flush')) {
|
||||
flush();
|
||||
}
|
||||
|
||||
// Sleep in small increments to be responsive to signals
|
||||
$sleepTime = $this->refreshInterval;
|
||||
while ($sleepTime > 0 && $this->running) {
|
||||
sleep(1);
|
||||
$sleepTime--;
|
||||
|
||||
// Process signals during sleep
|
||||
if (function_exists('pcntl_signal_dispatch')) {
|
||||
pcntl_signal_dispatch();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$cleanup();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a single snapshot of Redis stats without entering monitor mode.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getStats(): array
|
||||
{
|
||||
$info = $this->getRedisInfo();
|
||||
|
||||
if ($info === null) {
|
||||
return [
|
||||
'connected' => false,
|
||||
'host' => $this->host,
|
||||
'port' => $this->port,
|
||||
];
|
||||
}
|
||||
|
||||
return $this->parseRedisInfo($info);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse Redis INFO into structured data.
|
||||
*
|
||||
* @param array $info
|
||||
* @return array
|
||||
*/
|
||||
protected function parseRedisInfo(array $info): array
|
||||
{
|
||||
// Flatten nested arrays if present
|
||||
$flatInfo = [];
|
||||
foreach ($info as $key => $value) {
|
||||
if (is_array($value)) {
|
||||
$flatInfo = array_merge($flatInfo, $value);
|
||||
} else {
|
||||
$flatInfo[$key] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate hit rate
|
||||
$hits = (int) ($flatInfo['keyspace_hits'] ?? 0);
|
||||
$misses = (int) ($flatInfo['keyspace_misses'] ?? 0);
|
||||
$totalOps = $hits + $misses;
|
||||
$hitRate = $totalOps > 0 ? round(($hits * 100) / $totalOps, 2) : 0;
|
||||
|
||||
// Parse db keys from all databases
|
||||
$dbKeys = 0;
|
||||
for ($i = 0; $i <= 15; $i++) {
|
||||
$dbKey = "db{$i}";
|
||||
if (isset($flatInfo[$dbKey])) {
|
||||
$dbValue = $flatInfo[$dbKey];
|
||||
if (is_array($dbValue)) {
|
||||
// phpredis returns array with 'keys' key
|
||||
$dbKeys += (int) ($dbValue['keys'] ?? 0);
|
||||
} elseif (is_string($dbValue)) {
|
||||
// String format: "keys=123,expires=0,avg_ttl=0"
|
||||
if (preg_match('/keys=(\d+)/', $dbValue, $matches)) {
|
||||
$dbKeys += (int) $matches[1];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'connected' => true,
|
||||
'host' => $this->host,
|
||||
'port' => $this->port,
|
||||
'version' => $flatInfo['redis_version'] ?? 'unknown',
|
||||
'uptime_days' => (int) ($flatInfo['uptime_in_days'] ?? 0),
|
||||
'uptime_seconds' => (int) ($flatInfo['uptime_in_seconds'] ?? 0),
|
||||
'role' => $flatInfo['role'] ?? 'standalone',
|
||||
'connected_clients' => (int) ($flatInfo['connected_clients'] ?? 0),
|
||||
'blocked_clients' => (int) ($flatInfo['blocked_clients'] ?? 0),
|
||||
'total_connections' => (int) ($flatInfo['total_connections_received'] ?? 0),
|
||||
'connected_slaves' => (int) ($flatInfo['connected_slaves'] ?? 0),
|
||||
'used_memory' => (int) ($flatInfo['used_memory'] ?? 0),
|
||||
'used_memory_peak' => (int) ($flatInfo['used_memory_peak'] ?? 0),
|
||||
'used_memory_rss' => (int) ($flatInfo['used_memory_rss'] ?? 0),
|
||||
'maxmemory' => (int) ($flatInfo['maxmemory'] ?? 0),
|
||||
'mem_fragmentation_ratio' => $flatInfo['mem_fragmentation_ratio'] ?? 'N/A',
|
||||
'total_commands' => (int) ($flatInfo['total_commands_processed'] ?? 0),
|
||||
'ops_per_sec' => (int) ($flatInfo['instantaneous_ops_per_sec'] ?? 0),
|
||||
'input_kbps' => (float) ($flatInfo['instantaneous_input_kbps'] ?? 0),
|
||||
'output_kbps' => (float) ($flatInfo['instantaneous_output_kbps'] ?? 0),
|
||||
'db_keys' => $dbKeys,
|
||||
'keyspace_hits' => $hits,
|
||||
'keyspace_misses' => $misses,
|
||||
'hit_rate' => $hitRate,
|
||||
'expired_keys' => (int) ($flatInfo['expired_keys'] ?? 0),
|
||||
'evicted_keys' => (int) ($flatInfo['evicted_keys'] ?? 0),
|
||||
'rdb_changes' => (int) ($flatInfo['rdb_changes_since_last_save'] ?? 0),
|
||||
'rdb_last_save' => (int) ($flatInfo['rdb_last_save_time'] ?? 0),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the Redis monitor dashboard.
|
||||
*
|
||||
* @param array $info
|
||||
*/
|
||||
protected function displayDashboard(array $info): void
|
||||
{
|
||||
$stats = $this->parseRedisInfo($info);
|
||||
$currentTime = now()->format('Y-m-d H:i:s');
|
||||
|
||||
// Build the entire output as a single string for atomic write
|
||||
$buffer = '';
|
||||
|
||||
// Header
|
||||
$buffer .= $this->printHeader("🔴 REDIS MONITOR v{$stats['version']} | {$this->host}:{$this->port}");
|
||||
|
||||
// Server Status
|
||||
$buffer .= $this->printSection("Server Status");
|
||||
$buffer .= $this->printRow("Status", "● ONLINE", self::GREEN);
|
||||
$buffer .= $this->printRow("Role", $stats['role'], self::MAGENTA);
|
||||
$buffer .= $this->printRow("Uptime", "{$stats['uptime_days']} days (" . $this->formatNumber($stats['uptime_seconds']) . " seconds)", self::WHITE);
|
||||
$buffer .= $this->printRow("Last Updated", $currentTime, self::GRAY);
|
||||
|
||||
// Memory Usage
|
||||
$buffer .= $this->printSection("Memory Usage");
|
||||
$buffer .= $this->printRow("Used Memory", $this->formatBytes($stats['used_memory']), self::CYAN);
|
||||
$buffer .= $this->printRow("Peak Memory", $this->formatBytes($stats['used_memory_peak']), self::YELLOW);
|
||||
$buffer .= $this->printRow("RSS Memory", $this->formatBytes($stats['used_memory_rss']), self::WHITE);
|
||||
$buffer .= $this->printRow("Fragmentation Ratio", (string) $stats['mem_fragmentation_ratio'], self::WHITE);
|
||||
|
||||
if ($stats['maxmemory'] > 0) {
|
||||
$buffer .= $this->printMeter("Memory Usage", $stats['used_memory'], $stats['maxmemory']);
|
||||
}
|
||||
|
||||
// Clients & Connections
|
||||
$buffer .= $this->printSection("Clients & Connections");
|
||||
$buffer .= $this->printRow("Connected Clients", $this->formatNumber($stats['connected_clients']), self::GREEN);
|
||||
$buffer .= $this->printRow("Blocked Clients", $this->formatNumber($stats['blocked_clients']), self::YELLOW);
|
||||
$buffer .= $this->printRow("Total Connections", $this->formatNumber($stats['total_connections']), self::WHITE);
|
||||
if ($stats['connected_slaves'] > 0) {
|
||||
$buffer .= $this->printRow("Connected Slaves", (string) $stats['connected_slaves'], self::MAGENTA);
|
||||
}
|
||||
|
||||
// Performance Metrics
|
||||
$buffer .= $this->printSection("Performance Metrics");
|
||||
$buffer .= $this->printRow("Commands Processed", $this->formatNumber($stats['total_commands']), self::WHITE);
|
||||
$buffer .= $this->printRow("Operations/sec", $this->formatNumber($stats['ops_per_sec']) . " ops/s", self::GREEN);
|
||||
$buffer .= $this->printRow("Network Input", sprintf("%.2f KB/s", $stats['input_kbps']), self::CYAN);
|
||||
$buffer .= $this->printRow("Network Output", sprintf("%.2f KB/s", $stats['output_kbps']), self::CYAN);
|
||||
|
||||
// Keyspace Statistics
|
||||
$buffer .= $this->printSection("Keyspace Statistics");
|
||||
$buffer .= $this->printRow("Total Keys", $this->formatNumber($stats['db_keys']), self::WHITE);
|
||||
$buffer .= $this->printRow("Keyspace Hits", $this->formatNumber($stats['keyspace_hits']), self::GREEN);
|
||||
$buffer .= $this->printRow("Keyspace Misses", $this->formatNumber($stats['keyspace_misses']), self::RED);
|
||||
$buffer .= $this->printRow("Hit Rate", $stats['hit_rate'] . "%", self::CYAN);
|
||||
$buffer .= $this->printRow("Expired Keys", $this->formatNumber($stats['expired_keys']), self::YELLOW);
|
||||
$buffer .= $this->printRow("Evicted Keys", $this->formatNumber($stats['evicted_keys']), self::RED);
|
||||
|
||||
// Persistence
|
||||
$buffer .= $this->printSection("Persistence");
|
||||
$buffer .= $this->printRow("Changes Since Save", $this->formatNumber($stats['rdb_changes']), self::WHITE);
|
||||
if ($stats['rdb_last_save'] > 0) {
|
||||
$lastSave = date('Y-m-d H:i:s', $stats['rdb_last_save']);
|
||||
$buffer .= $this->printRow("Last Save", $lastSave, self::GRAY);
|
||||
}
|
||||
|
||||
$buffer .= $this->printFooter();
|
||||
$buffer .= self::DIM . " Press Ctrl+C to exit | Refresh: {$this->refreshInterval}s" . self::NC . self::CLEAR_EOL . "\n";
|
||||
|
||||
// Clear any remaining content below
|
||||
$buffer .= "\033[J";
|
||||
|
||||
// Output the entire buffer at once
|
||||
echo $buffer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Display connection error screen.
|
||||
*/
|
||||
protected function displayConnectionError(): void
|
||||
{
|
||||
$buffer = '';
|
||||
$buffer .= $this->printHeader("⚠ REDIS MONITOR - CONNECTION FAILED");
|
||||
$buffer .= $this->printRow("Status", "● OFFLINE - Cannot connect to Redis", self::RED);
|
||||
$buffer .= $this->printRow("Host", "{$this->host}:{$this->port}", self::YELLOW);
|
||||
$buffer .= $this->printRow("Retrying in", "{$this->refreshInterval} seconds...", self::GRAY);
|
||||
$buffer .= $this->printFooter();
|
||||
$buffer .= self::DIM . " Press Ctrl+C to exit | Refresh: {$this->refreshInterval}s" . self::NC . self::CLEAR_EOL . "\n";
|
||||
$buffer .= "\033[J";
|
||||
|
||||
echo $buffer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Print dashboard header.
|
||||
*/
|
||||
protected function printHeader(string $title): string
|
||||
{
|
||||
// Strip emoji for length calculation (emoji can be multi-byte)
|
||||
$titleLen = mb_strlen(preg_replace('/[\x{1F300}-\x{1F9FF}]/u', ' ', $title));
|
||||
$padding = max(0, (int) (($this->boxWidth - $titleLen - 2) / 2));
|
||||
$line = str_repeat(self::H_LINE, $this->boxWidth);
|
||||
|
||||
$buffer = self::CYAN . self::TL_CORNER . $line . self::TR_CORNER . self::NC . self::CLEAR_EOL . "\n";
|
||||
$buffer .= self::CYAN . self::V_LINE . self::NC;
|
||||
$buffer .= str_repeat(' ', $padding);
|
||||
$buffer .= self::BOLD . self::WHITE . $title . self::NC;
|
||||
$buffer .= str_repeat(' ', max(0, $this->boxWidth - $padding - $titleLen));
|
||||
$buffer .= self::CYAN . self::V_LINE . self::NC . self::CLEAR_EOL . "\n";
|
||||
$buffer .= self::CYAN . self::T_RIGHT . $line . self::T_LEFT . self::NC . self::CLEAR_EOL . "\n";
|
||||
|
||||
return $buffer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Print section header.
|
||||
*/
|
||||
protected function printSection(string $title): string
|
||||
{
|
||||
$remainingWidth = $this->boxWidth - mb_strlen($title) - 5;
|
||||
$line = str_repeat(self::H_LINE, max(0, $remainingWidth));
|
||||
|
||||
return self::CYAN . self::T_RIGHT . self::H_LINE . self::H_LINE . self::NC . " "
|
||||
. self::YELLOW . self::BOLD . $title . self::NC . " "
|
||||
. self::CYAN . $line . self::T_LEFT . self::NC . self::CLEAR_EOL . "\n";
|
||||
}
|
||||
|
||||
/**
|
||||
* Print a data row.
|
||||
*/
|
||||
protected function printRow(string $label, string $value, string $color = self::WHITE): string
|
||||
{
|
||||
$labelWidth = 25;
|
||||
$valueWidth = $this->boxWidth - $labelWidth - 3;
|
||||
|
||||
$label = str_pad($label, $labelWidth);
|
||||
$value = str_pad(mb_substr($value, 0, $valueWidth), $valueWidth);
|
||||
|
||||
return self::CYAN . self::V_LINE . self::NC . " "
|
||||
. self::DIM . $label . self::NC . " "
|
||||
. $color . $value . self::NC
|
||||
. self::CYAN . self::V_LINE . self::NC . self::CLEAR_EOL . "\n";
|
||||
}
|
||||
|
||||
/**
|
||||
* Print a progress meter.
|
||||
*/
|
||||
protected function printMeter(string $label, int $current, int $max): string
|
||||
{
|
||||
$labelWidth = 25;
|
||||
$meterWidth = min(30, $this->boxWidth - $labelWidth - 10);
|
||||
|
||||
$percentage = $max > 0 ? (int) (($current * 100) / $max) : 0;
|
||||
$filled = (int) (($percentage * $meterWidth) / 100);
|
||||
$empty = $meterWidth - $filled;
|
||||
|
||||
// Choose color based on percentage
|
||||
$color = self::GREEN;
|
||||
if ($percentage > 80) {
|
||||
$color = self::RED;
|
||||
} elseif ($percentage > 60) {
|
||||
$color = self::YELLOW;
|
||||
}
|
||||
|
||||
$meter = $color . str_repeat("█", $filled) . self::GRAY . str_repeat("░", $empty) . self::NC;
|
||||
|
||||
$label = str_pad($label, $labelWidth);
|
||||
$percentStr = sprintf("%3d%%", $percentage);
|
||||
$remainingSpace = $this->boxWidth - $labelWidth - $meterWidth - strlen($percentStr) - 4;
|
||||
|
||||
return self::CYAN . self::V_LINE . self::NC . " "
|
||||
. self::DIM . $label . self::NC . " "
|
||||
. $meter . " " . self::WHITE . $percentStr . self::NC
|
||||
. str_repeat(' ', max(0, $remainingSpace))
|
||||
. self::CYAN . self::V_LINE . self::NC . self::CLEAR_EOL . "\n";
|
||||
}
|
||||
|
||||
/**
|
||||
* Print dashboard footer.
|
||||
*/
|
||||
protected function printFooter(): string
|
||||
{
|
||||
$line = str_repeat(self::H_LINE, $this->boxWidth);
|
||||
return self::CYAN . self::BL_CORNER . $line . self::BR_CORNER . self::NC . self::CLEAR_EOL . "\n";
|
||||
}
|
||||
|
||||
/**
|
||||
* Format bytes into human-readable string.
|
||||
*/
|
||||
protected function formatBytes(int $bytes): string
|
||||
{
|
||||
if ($bytes >= 1073741824) {
|
||||
return sprintf("%.2f GB", $bytes / 1073741824);
|
||||
} elseif ($bytes >= 1048576) {
|
||||
return sprintf("%.2f MB", $bytes / 1048576);
|
||||
} elseif ($bytes >= 1024) {
|
||||
return sprintf("%.2f KB", $bytes / 1024);
|
||||
}
|
||||
return $bytes . " B";
|
||||
}
|
||||
|
||||
/**
|
||||
* Format number with thousands separators.
|
||||
*/
|
||||
protected function formatNumber(int $number): string
|
||||
{
|
||||
return number_format($number);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide terminal cursor.
|
||||
*/
|
||||
protected function hideCursor(): void
|
||||
{
|
||||
echo "\033[?25l";
|
||||
}
|
||||
|
||||
/**
|
||||
* Show terminal cursor.
|
||||
*/
|
||||
protected function showCursor(): void
|
||||
{
|
||||
echo "\033[?25h";
|
||||
}
|
||||
|
||||
/**
|
||||
* Move cursor to top-left.
|
||||
*/
|
||||
protected function moveToTop(): void
|
||||
{
|
||||
echo "\033[H";
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the screen.
|
||||
*/
|
||||
protected function clearScreen(): void
|
||||
{
|
||||
echo "\033[2J\033[H";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -233,25 +233,34 @@ class TmuxLayoutBuilder
|
||||
}
|
||||
|
||||
// redis monitoring
|
||||
if ((int) Settings::settingValue('redis') === 1 && $this->commandExists('redis-cli')) {
|
||||
$redisHost = config('database.redis.default.host', '127.0.0.1');
|
||||
$redisPort = config('database.redis.default.port', 6379);
|
||||
if ((int) Settings::settingValue('redis') === 1) {
|
||||
$redisArgs = Settings::settingValue('redis_args') ?? '';
|
||||
$refreshInterval = 2;
|
||||
$refreshInterval = 30;
|
||||
|
||||
$this->paneManager->createWindow($windowIndex, 'redis');
|
||||
|
||||
// Check if custom args provided for simple redis-cli output
|
||||
if (! empty($redisArgs) && $redisArgs !== 'NULL') {
|
||||
if (! empty($redisArgs) && $redisArgs !== 'NULL' && $this->commandExists('redis-cli')) {
|
||||
$redisHost = config('database.redis.default.host', '127.0.0.1');
|
||||
$redisPort = config('database.redis.default.port', 6379);
|
||||
$this->paneManager->respawnPane("{$windowIndex}.0", "watch -n{$refreshInterval} -c 'redis-cli -h {$redisHost} -p {$redisPort} {$redisArgs}'");
|
||||
} else {
|
||||
// Use modern visual monitoring script
|
||||
$monitorScript = base_path('misc/redis-monitor.sh');
|
||||
if (file_exists($monitorScript)) {
|
||||
$this->paneManager->respawnPane("{$windowIndex}.0", "bash {$monitorScript} {$redisHost} {$redisPort} {$refreshInterval}");
|
||||
// Use PHP-based Redis monitor service
|
||||
$redisHost = config('database.redis.default.host', '127.0.0.1');
|
||||
$redisPort = config('database.redis.default.port', 6379);
|
||||
$artisan = base_path('artisan');
|
||||
|
||||
// Determine how to connect to Redis
|
||||
$connectionInfo = $this->resolveRedisConnection($redisHost, (int) $redisPort);
|
||||
|
||||
if ($connectionInfo['use_sail']) {
|
||||
// Use sail to run inside Docker container
|
||||
$sail = base_path('sail');
|
||||
$this->paneManager->respawnPane("{$windowIndex}.0", "{$sail} artisan redis:monitor --refresh={$refreshInterval}");
|
||||
} else {
|
||||
// Fallback to basic redis-cli info with color
|
||||
$this->paneManager->respawnPane("{$windowIndex}.0", "watch -n{$refreshInterval} -c 'redis-cli -h {$redisHost} -p {$redisPort} info'");
|
||||
// Run directly, potentially with host override
|
||||
$envPrefix = $connectionInfo['override_host'] ? "REDIS_HOST={$connectionInfo['host']} " : '';
|
||||
$this->paneManager->respawnPane("{$windowIndex}.0", "{$envPrefix}php {$artisan} redis:monitor --refresh={$refreshInterval}");
|
||||
}
|
||||
}
|
||||
$windowIndex++;
|
||||
@@ -274,4 +283,39 @@ class TmuxLayoutBuilder
|
||||
|
||||
return $result->successful() && str_contains($result->output(), $command);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve how to connect to Redis from the host
|
||||
*
|
||||
* Returns an array with:
|
||||
* - 'use_sail' => bool - whether to use sail to run inside Docker
|
||||
* - 'override_host' => bool - whether to override REDIS_HOST env var
|
||||
* - 'host' => string - the host to use (only relevant if override_host is true)
|
||||
*/
|
||||
protected function resolveRedisConnection(string $host, int $port): array
|
||||
{
|
||||
// If host is already an IP or localhost, use it directly
|
||||
if (filter_var($host, FILTER_VALIDATE_IP) || $host === 'localhost') {
|
||||
return ['use_sail' => false, 'override_host' => false, 'host' => $host];
|
||||
}
|
||||
|
||||
// Try to resolve the hostname
|
||||
$resolved = gethostbyname($host);
|
||||
if ($resolved !== $host) {
|
||||
// Hostname resolves - use it directly
|
||||
return ['use_sail' => false, 'override_host' => false, 'host' => $host];
|
||||
}
|
||||
|
||||
// Hostname doesn't resolve (Docker internal hostname)
|
||||
// Check if Redis is accessible on 127.0.0.1 (Docker port forwarding)
|
||||
$socket = @fsockopen('127.0.0.1', $port, $errno, $errstr, 1);
|
||||
if ($socket !== false) {
|
||||
fclose($socket);
|
||||
// Redis accessible on localhost - override host to 127.0.0.1
|
||||
return ['use_sail' => false, 'override_host' => true, 'host' => '127.0.0.1'];
|
||||
}
|
||||
|
||||
// Redis not accessible on localhost - need to use sail
|
||||
return ['use_sail' => true, 'override_host' => false, 'host' => $host];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,266 +0,0 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Redis Monitor - Modern visual monitoring for Redis server
|
||||
# Usage: redis-monitor.sh [host] [port] [refresh_interval] [password]
|
||||
# redis-monitor.sh 127.0.0.1 6379 30 mypassword
|
||||
#
|
||||
# Or set REDIS_PASSWORD environment variable:
|
||||
# export REDIS_PASSWORD=mypassword
|
||||
# redis-monitor.sh
|
||||
#
|
||||
|
||||
# Configuration
|
||||
REDIS_HOST="${1:-127.0.0.1}"
|
||||
REDIS_PORT="${2:-6379}"
|
||||
REFRESH="${3:-30}"
|
||||
REDIS_PASSWORD="${4:-${REDIS_PASSWORD}}" # Accept as arg or env var
|
||||
|
||||
# Colors
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
MAGENTA='\033[0;35m'
|
||||
CYAN='\033[0;36m'
|
||||
WHITE='\033[1;37m'
|
||||
GRAY='\033[0;90m'
|
||||
BOLD='\033[1m'
|
||||
DIM='\033[2m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Box drawing characters
|
||||
H_LINE="─"
|
||||
V_LINE="│"
|
||||
TL_CORNER="┌"
|
||||
TR_CORNER="┐"
|
||||
BL_CORNER="└"
|
||||
BR_CORNER="┘"
|
||||
T_DOWN="┬"
|
||||
T_UP="┴"
|
||||
T_RIGHT="├"
|
||||
T_LEFT="┤"
|
||||
CROSS="┼"
|
||||
|
||||
# Get terminal width
|
||||
TERM_WIDTH=$(tput cols 2>/dev/null || echo 80)
|
||||
BOX_WIDTH=$((TERM_WIDTH - 2))
|
||||
|
||||
# Clear to end of line escape sequence
|
||||
CLEAR_EOL='\033[K'
|
||||
|
||||
# Functions
|
||||
print_header() {
|
||||
local title="$1"
|
||||
local padding=$(( (BOX_WIDTH - ${#title} - 2) / 2 ))
|
||||
echo -e "${CYAN}${TL_CORNER}$(printf '%*s' "$BOX_WIDTH" | tr ' ' "$H_LINE")${TR_CORNER}${NC}${CLEAR_EOL}"
|
||||
echo -e "${CYAN}${V_LINE}${NC}$(printf '%*s' "$padding")${BOLD}${WHITE}$title${NC}$(printf '%*s' "$((BOX_WIDTH - padding - ${#title}))")${CYAN}${V_LINE}${NC}${CLEAR_EOL}"
|
||||
echo -e "${CYAN}${T_RIGHT}$(printf '%*s' "$BOX_WIDTH" | tr ' ' "$H_LINE")${T_LEFT}${NC}${CLEAR_EOL}"
|
||||
}
|
||||
|
||||
print_section() {
|
||||
local title="$1"
|
||||
echo -e "${CYAN}${T_RIGHT}${H_LINE}${H_LINE}${NC} ${YELLOW}${BOLD}$title${NC} ${CYAN}$(printf '%*s' "$((BOX_WIDTH - ${#title} - 5))" | tr ' ' "$H_LINE")${T_LEFT}${NC}${CLEAR_EOL}"
|
||||
}
|
||||
|
||||
print_row() {
|
||||
local label="$1"
|
||||
local value="$2"
|
||||
local color="${3:-$WHITE}"
|
||||
local label_width=25
|
||||
local value_width=$((BOX_WIDTH - label_width - 3))
|
||||
printf "${CYAN}${V_LINE}${NC} ${DIM}%-${label_width}s${NC} ${color}%-${value_width}s${NC}${CYAN}${V_LINE}${NC}${CLEAR_EOL}\n" "$label" "$value"
|
||||
}
|
||||
|
||||
print_meter() {
|
||||
local label="$1"
|
||||
local current="$2"
|
||||
local max="$3"
|
||||
local label_width=25
|
||||
local meter_width=30
|
||||
local percentage=0
|
||||
|
||||
if [ "$max" -gt 0 ] 2>/dev/null; then
|
||||
percentage=$((current * 100 / max))
|
||||
fi
|
||||
|
||||
local filled=$((percentage * meter_width / 100))
|
||||
local empty=$((meter_width - filled))
|
||||
|
||||
local color=$GREEN
|
||||
if [ "$percentage" -gt 80 ]; then
|
||||
color=$RED
|
||||
elif [ "$percentage" -gt 60 ]; then
|
||||
color=$YELLOW
|
||||
fi
|
||||
|
||||
local meter="${color}"
|
||||
for ((i=0; i<filled; i++)); do meter+="█"; done
|
||||
meter+="${GRAY}"
|
||||
for ((i=0; i<empty; i++)); do meter+="░"; done
|
||||
meter+="${NC}"
|
||||
|
||||
printf "${CYAN}${V_LINE}${NC} ${DIM}%-${label_width}s${NC} %b ${WHITE}%3d%%${NC}${CYAN}${V_LINE}${NC}${CLEAR_EOL}\n" "$label" "$meter" "$percentage"
|
||||
}
|
||||
|
||||
print_footer() {
|
||||
echo -e "${CYAN}${BL_CORNER}$(printf '%*s' "$BOX_WIDTH" | tr ' ' "$H_LINE")${BR_CORNER}${NC}${CLEAR_EOL}"
|
||||
}
|
||||
|
||||
format_bytes() {
|
||||
local bytes=$1
|
||||
if [ "$bytes" -ge 1073741824 ]; then
|
||||
echo "$(echo "scale=2; $bytes / 1073741824" | bc) GB"
|
||||
elif [ "$bytes" -ge 1048576 ]; then
|
||||
echo "$(echo "scale=2; $bytes / 1048576" | bc) MB"
|
||||
elif [ "$bytes" -ge 1024 ]; then
|
||||
echo "$(echo "scale=2; $bytes / 1024" | bc) KB"
|
||||
else
|
||||
echo "$bytes B"
|
||||
fi
|
||||
}
|
||||
|
||||
format_number() {
|
||||
echo "$1" | sed ':a;s/\B[0-9]\{3\}\>$/,&/;ta'
|
||||
}
|
||||
|
||||
get_redis_info() {
|
||||
if [ -n "$REDIS_PASSWORD" ]; then
|
||||
redis-cli -h "$REDIS_HOST" -p "$REDIS_PORT" -a "$REDIS_PASSWORD" --no-auth-warning info 2>/dev/null
|
||||
else
|
||||
redis-cli -h "$REDIS_HOST" -p "$REDIS_PORT" info 2>/dev/null
|
||||
fi
|
||||
}
|
||||
|
||||
# Terminal control for flicker-free updates
|
||||
hide_cursor() { printf '\033[?25l'; }
|
||||
show_cursor() { printf '\033[?25h'; }
|
||||
move_to_top() { printf '\033[H'; }
|
||||
clear_screen() { printf '\033[2J\033[H'; }
|
||||
|
||||
# Cleanup on exit
|
||||
cleanup() {
|
||||
show_cursor
|
||||
printf '\033[?1049l' # Exit alternate screen buffer
|
||||
exit 0
|
||||
}
|
||||
trap cleanup EXIT INT TERM
|
||||
|
||||
# Enter alternate screen buffer and hide cursor
|
||||
printf '\033[?1049h'
|
||||
hide_cursor
|
||||
clear_screen
|
||||
|
||||
# Main loop
|
||||
while true; do
|
||||
# Move cursor to top-left instead of clearing (prevents flicker)
|
||||
move_to_top
|
||||
|
||||
# Get Redis info
|
||||
INFO=$(get_redis_info)
|
||||
|
||||
if [ -z "$INFO" ]; then
|
||||
clear_screen
|
||||
print_header "⚠ REDIS MONITOR - CONNECTION FAILED"
|
||||
print_row "Status" "Cannot connect to Redis" "$RED"
|
||||
print_row "Host" "$REDIS_HOST:$REDIS_PORT" "$YELLOW"
|
||||
print_footer
|
||||
echo -e "${DIM}Press Ctrl+C to exit | Refresh: ${REFRESH}s | Retrying...${NC}"
|
||||
sleep "$REFRESH"
|
||||
continue
|
||||
fi
|
||||
|
||||
# Parse info
|
||||
REDIS_VERSION=$(echo "$INFO" | grep "^redis_version:" | cut -d: -f2 | tr -d '\r')
|
||||
UPTIME_DAYS=$(echo "$INFO" | grep "^uptime_in_days:" | cut -d: -f2 | tr -d '\r')
|
||||
UPTIME_SECONDS=$(echo "$INFO" | grep "^uptime_in_seconds:" | cut -d: -f2 | tr -d '\r')
|
||||
CONNECTED_CLIENTS=$(echo "$INFO" | grep "^connected_clients:" | cut -d: -f2 | tr -d '\r')
|
||||
BLOCKED_CLIENTS=$(echo "$INFO" | grep "^blocked_clients:" | cut -d: -f2 | tr -d '\r')
|
||||
USED_MEMORY=$(echo "$INFO" | grep "^used_memory:" | cut -d: -f2 | tr -d '\r')
|
||||
USED_MEMORY_PEAK=$(echo "$INFO" | grep "^used_memory_peak:" | cut -d: -f2 | tr -d '\r')
|
||||
USED_MEMORY_RSS=$(echo "$INFO" | grep "^used_memory_rss:" | cut -d: -f2 | tr -d '\r')
|
||||
MAXMEMORY=$(echo "$INFO" | grep "^maxmemory:" | cut -d: -f2 | tr -d '\r')
|
||||
TOTAL_CONNECTIONS=$(echo "$INFO" | grep "^total_connections_received:" | cut -d: -f2 | tr -d '\r')
|
||||
TOTAL_COMMANDS=$(echo "$INFO" | grep "^total_commands_processed:" | cut -d: -f2 | tr -d '\r')
|
||||
OPS_PER_SEC=$(echo "$INFO" | grep "^instantaneous_ops_per_sec:" | cut -d: -f2 | tr -d '\r')
|
||||
INPUT_KBPS=$(echo "$INFO" | grep "^instantaneous_input_kbps:" | cut -d: -f2 | tr -d '\r')
|
||||
OUTPUT_KBPS=$(echo "$INFO" | grep "^instantaneous_output_kbps:" | cut -d: -f2 | tr -d '\r')
|
||||
KEYSPACE_HITS=$(echo "$INFO" | grep "^keyspace_hits:" | cut -d: -f2 | tr -d '\r')
|
||||
KEYSPACE_MISSES=$(echo "$INFO" | grep "^keyspace_misses:" | cut -d: -f2 | tr -d '\r')
|
||||
EXPIRED_KEYS=$(echo "$INFO" | grep "^expired_keys:" | cut -d: -f2 | tr -d '\r')
|
||||
EVICTED_KEYS=$(echo "$INFO" | grep "^evicted_keys:" | cut -d: -f2 | tr -d '\r')
|
||||
DB_KEYS=$(echo "$INFO" | grep "^db0:" | sed 's/.*keys=\([0-9]*\).*/\1/' | tr -d '\r')
|
||||
ROLE=$(echo "$INFO" | grep "^role:" | cut -d: -f2 | tr -d '\r')
|
||||
CONNECTED_SLAVES=$(echo "$INFO" | grep "^connected_slaves:" | cut -d: -f2 | tr -d '\r')
|
||||
MEM_FRAG_RATIO=$(echo "$INFO" | grep "^mem_fragmentation_ratio:" | cut -d: -f2 | tr -d '\r')
|
||||
RDB_CHANGES=$(echo "$INFO" | grep "^rdb_changes_since_last_save:" | cut -d: -f2 | tr -d '\r')
|
||||
RDB_LAST_SAVE=$(echo "$INFO" | grep "^rdb_last_save_time:" | cut -d: -f2 | tr -d '\r')
|
||||
|
||||
# Calculate hit rate
|
||||
HIT_RATE=0
|
||||
if [ -n "$KEYSPACE_HITS" ] && [ -n "$KEYSPACE_MISSES" ]; then
|
||||
TOTAL_OPS=$((KEYSPACE_HITS + KEYSPACE_MISSES))
|
||||
if [ "$TOTAL_OPS" -gt 0 ]; then
|
||||
HIT_RATE=$((KEYSPACE_HITS * 100 / TOTAL_OPS))
|
||||
fi
|
||||
fi
|
||||
|
||||
# Current time
|
||||
CURRENT_TIME=$(date "+%Y-%m-%d %H:%M:%S")
|
||||
|
||||
# Print dashboard
|
||||
print_header "🔴 REDIS MONITOR v${REDIS_VERSION:-unknown} | ${REDIS_HOST}:${REDIS_PORT}"
|
||||
|
||||
print_section "Server Status"
|
||||
print_row "Status" "● ONLINE" "$GREEN"
|
||||
print_row "Role" "${ROLE:-standalone}" "$MAGENTA"
|
||||
print_row "Uptime" "${UPTIME_DAYS:-0} days ($(format_number ${UPTIME_SECONDS:-0}) seconds)" "$WHITE"
|
||||
print_row "Last Updated" "$CURRENT_TIME" "$GRAY"
|
||||
|
||||
print_section "Memory Usage"
|
||||
USED_MEM_FMT=$(format_bytes ${USED_MEMORY:-0})
|
||||
PEAK_MEM_FMT=$(format_bytes ${USED_MEMORY_PEAK:-0})
|
||||
RSS_MEM_FMT=$(format_bytes ${USED_MEMORY_RSS:-0})
|
||||
print_row "Used Memory" "$USED_MEM_FMT" "$CYAN"
|
||||
print_row "Peak Memory" "$PEAK_MEM_FMT" "$YELLOW"
|
||||
print_row "RSS Memory" "$RSS_MEM_FMT" "$WHITE"
|
||||
print_row "Fragmentation Ratio" "${MEM_FRAG_RATIO:-N/A}" "$WHITE"
|
||||
|
||||
if [ -n "$MAXMEMORY" ] && [ "$MAXMEMORY" -gt 0 ]; then
|
||||
print_meter "Memory Usage" "${USED_MEMORY:-0}" "$MAXMEMORY"
|
||||
fi
|
||||
|
||||
print_section "Clients & Connections"
|
||||
print_row "Connected Clients" "$(format_number ${CONNECTED_CLIENTS:-0})" "$GREEN"
|
||||
print_row "Blocked Clients" "$(format_number ${BLOCKED_CLIENTS:-0})" "$YELLOW"
|
||||
print_row "Total Connections" "$(format_number ${TOTAL_CONNECTIONS:-0})" "$WHITE"
|
||||
[ -n "$CONNECTED_SLAVES" ] && print_row "Connected Slaves" "$CONNECTED_SLAVES" "$MAGENTA"
|
||||
|
||||
print_section "Performance Metrics"
|
||||
print_row "Commands Processed" "$(format_number ${TOTAL_COMMANDS:-0})" "$WHITE"
|
||||
print_row "Operations/sec" "$(format_number ${OPS_PER_SEC:-0}) ops/s" "$GREEN"
|
||||
print_row "Network Input" "${INPUT_KBPS:-0} KB/s" "$CYAN"
|
||||
print_row "Network Output" "${OUTPUT_KBPS:-0} KB/s" "$CYAN"
|
||||
|
||||
print_section "Keyspace Statistics"
|
||||
print_row "Total Keys (db0)" "$(format_number ${DB_KEYS:-0})" "$WHITE"
|
||||
print_row "Keyspace Hits" "$(format_number ${KEYSPACE_HITS:-0})" "$GREEN"
|
||||
print_row "Keyspace Misses" "$(format_number ${KEYSPACE_MISSES:-0})" "$RED"
|
||||
print_row "Hit Rate" "${HIT_RATE}%" "$CYAN"
|
||||
print_row "Expired Keys" "$(format_number ${EXPIRED_KEYS:-0})" "$YELLOW"
|
||||
print_row "Evicted Keys" "$(format_number ${EVICTED_KEYS:-0})" "$RED"
|
||||
|
||||
print_section "Persistence"
|
||||
print_row "Changes Since Save" "$(format_number ${RDB_CHANGES:-0})" "$WHITE"
|
||||
if [ -n "$RDB_LAST_SAVE" ] && [ "$RDB_LAST_SAVE" -gt 0 ]; then
|
||||
LAST_SAVE_FMT=$(date -d "@$RDB_LAST_SAVE" "+%Y-%m-%d %H:%M:%S" 2>/dev/null || echo "N/A")
|
||||
print_row "Last Save" "$LAST_SAVE_FMT" "$GRAY"
|
||||
fi
|
||||
|
||||
print_footer
|
||||
echo -e "${DIM}Press Ctrl+C to exit | Refresh: ${REFRESH}s${NC}${CLEAR_EOL}"
|
||||
# Clear any remaining lines below (in case previous frame had more content)
|
||||
printf '\033[J'
|
||||
|
||||
sleep "$REFRESH"
|
||||
done
|
||||
|
||||
Reference in New Issue
Block a user