Make redis monitor a service

This commit is contained in:
DariusIII
2025-12-31 11:53:01 +01:00
parent ab0700d750
commit 0380f637aa
4 changed files with 725 additions and 277 deletions
+146
View File
@@ -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";
}
}
+524
View File
@@ -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";
}
}
+55 -11
View File
@@ -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];
}
}
-266
View File
@@ -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