#!/usr/bin/env bun /** * Redis/Dragonfly Connection Monitor - Simplified Version * * Shows active connections grouped by client name with basic stats * * Usage: * bun run scripts/get-redis-connections.ts # Show connections once * bun run scripts/get-redis-connections.ts --monitor # Monitor in real-time */ import Redis from 'ioredis'; interface ClientInfo { id: string; name?: string; addr: string; age: string; idle: string; cmd: string; tot?: string; fd?: string; flags?: string; db?: string; sub?: string; psub?: string; multi?: string; qbuf?: string; qbufFree?: string; obl?: string; oll?: string; omem?: string; events?: string; [key: string]: string | undefined; // Index signature for dynamic properties } interface GroupedConnections { [clientName: string]: { count: number; connections: ClientInfo[]; totalCommands: number; avgIdleTime: number; }; } class RedisConnectionMonitor { private redis: Redis; constructor() { this.redis = new Redis({ host: process.env.DRAGONFLY_HOST || 'localhost', port: parseInt(process.env.DRAGONFLY_PORT || '6379'), password: process.env.DRAGONFLY_PASSWORD || undefined, db: parseInt(process.env.DRAGONFLY_DATABASE || '0'), maxRetriesPerRequest: 3, connectTimeout: 10000, lazyConnect: true, }); } /** * Parse CLIENT LIST output into structured data */ private parseClientList(clientListOutput: string): ClientInfo[] { const lines = clientListOutput.trim().split('\n'); const clients: ClientInfo[] = []; for (const line of lines) { if (!line.trim()) continue; const client: ClientInfo = { id: '', addr: '', age: '', idle: '', cmd: '', }; // Parse key=value pairs const pairs = line.split(' '); for (const pair of pairs) { const [key, value] = pair.split('='); if (key && value !== undefined) { client[key] = value; } } clients.push(client); } return clients; } /** * Clean up client names by removing timestamp/number suffixes */ private cleanClientName(rawName: string): string { if (!rawName || rawName === 'unnamed') { return rawName; } // Remove timestamp/number suffixes (like BATCH-PROCESSOR-1749573709724) // Pattern: Remove dash followed by numbers at the end let cleanName = rawName.replace(/-\d+$/, ''); // Also handle patterns like NAME---1-1- (RedisInsight style) cleanName = cleanName.replace(/---\d+-\d+-?$/, ''); // Convert to a more readable format cleanName = cleanName .replace(/-/g, ' ') .toLowerCase() .replace(/\b\w/g, l => l.toUpperCase()); return cleanName; } /** * Group connections by client name/type */ private groupConnectionsByName(clients: ClientInfo[]): GroupedConnections { const grouped: GroupedConnections = {}; for (const client of clients) { // Determine client name - use 'name' field if set, otherwise infer from other properties let clientName = client.name || 'unnamed'; // Clean up the client name by removing timestamps/numbers if (clientName && clientName !== 'unnamed') { clientName = this.cleanClientName(clientName); } // If no name is set, try to infer from the connection characteristics if (clientName === 'unnamed' || !clientName) { if (client.flags?.includes('M')) { clientName = 'Master Connection'; } else if (client.flags?.includes('S')) { clientName = 'Slave Connection'; } else if (client.flags?.includes('P')) { clientName = 'PubSub Connection'; } else if (client.cmd === 'monitor') { clientName = 'Monitor Connection'; } else if (client.addr.includes('127.0.0.1') || client.addr.includes('localhost')) { clientName = 'Local Client'; } else if (client.addr.includes('172.') || client.addr.includes('192.168.')) { clientName = 'Docker Network Client'; } else { clientName = `Client ${client.addr.split(':')[0]}`; } } if (!grouped[clientName]) { grouped[clientName] = { count: 0, connections: [], totalCommands: 0, avgIdleTime: 0, }; } grouped[clientName].count++; grouped[clientName].connections.push(client); // Parse command count if available const cmdCount = client.tot ? parseInt(client.tot) : 0; grouped[clientName].totalCommands += cmdCount; } // Calculate average idle times for (const groupName in grouped) { const group = grouped[groupName]; const totalIdle = group.connections.reduce((sum, client) => { return sum + parseInt(client.idle || '0'); }, 0); group.avgIdleTime = group.count > 0 ? Math.round(totalIdle / group.count) : 0; } return grouped; } /** * Format connection details for display */ private formatConnectionDetails(client: ClientInfo): string { const details = [ `ID: ${client.id}`, `Address: ${client.addr}`, `Age: ${client.age}s`, `Idle: ${client.idle}s`, ]; if (client.tot) { details.push(`Total Commands: ${client.tot}`); } return details.join(', '); } /** * Get and display all connections grouped by name */ async getConnectionsGroupedByName(): Promise { try { console.log('šŸ” Connecting to Dragonfly...'); // Check if already connected, if not connect if (this.redis.status !== 'ready') { await this.redis.connect(); } console.log('šŸ“” Fetching client connections...'); const clientListOutput = await this.redis.client('LIST') as string; if (!clientListOutput) { console.log('āŒ No client information available'); return; } const clients = this.parseClientList(clientListOutput); const grouped = this.groupConnectionsByName(clients); // Get server and memory info first const info = await this.redis.info('server'); const memoryInfo = await this.redis.info('memory'); // Parse server info const infoLines = info.split('\n'); let version = '', uptime = '', connectedClients = ''; for (const line of infoLines) { if (line.includes('dragonfly_version')) version = line.split(':')[1]?.trim() || ''; if (line.includes('uptime_in_seconds')) { const seconds = parseInt(line.split(':')[1]?.trim() || '0'); const days = Math.floor(seconds / 86400); const hours = Math.floor((seconds % 86400) / 3600); uptime = `${days}d ${hours}h`; } if (line.includes('connected_clients')) connectedClients = line.split(':')[1]?.trim() || ''; } // Parse memory info const memoryLines = memoryInfo.split('\n'); let usedMemory = '', maxMemory = '', memoryPercent = ''; for (const line of memoryLines) { if (line.includes('used_memory_human')) usedMemory = line.split(':')[1]?.trim() || ''; if (line.includes('maxmemory_human')) maxMemory = line.split(':')[1]?.trim() || ''; if (line.includes('used_memory_percentage')) memoryPercent = line.split(':')[1]?.trim() || ''; } console.log('\nšŸŽÆ DRAGONFLY CONNECTION SUMMARY'); console.log('═'.repeat(60)); console.log(`šŸ”— Connections: ${clients.length} active (${Object.keys(grouped).length} groups) | šŸ–„ļø Version: ${version} | ā±ļø Uptime: ${uptime}`); console.log(`šŸ’¾ Memory: ${usedMemory}${maxMemory ? ` / ${maxMemory}` : ''}${memoryPercent ? ` (${memoryPercent}%)` : ''}`); console.log(''); // Sort groups by count (most connections first) const sortedGroups = Object.entries(grouped).sort((a, b) => b[1].count - a[1].count); // Display grouped connections for (const [groupName, group] of sortedGroups) { console.log(`šŸ“‚ ${groupName.toUpperCase()} (${group.count} connections, ${group.totalCommands} commands, ${group.avgIdleTime}s avg idle)`); for (const client of group.connections) { console.log(` ā”œā”€ ${this.formatConnectionDetails(client)}`); } } } catch (error) { console.error('āŒ Error connecting to Dragonfly:', error); console.log('\nšŸ’” Troubleshooting:'); console.log(' - Make sure Dragonfly Docker container is running'); console.log(' - Check if port 6379 is accessible'); console.log(' - Verify environment variables if using custom connection settings'); console.log(' - Run: docker ps | grep dragonfly'); } finally { // Only disconnect if we're not in monitoring mode if (this.redis.status === 'ready') { await this.redis.disconnect(); } } } /** * Monitor connections in real-time */ async monitorConnections(intervalSeconds: number = 5): Promise { console.log(`šŸ”„ Starting real-time monitoring (refresh every ${intervalSeconds}s)`); console.log('Press Ctrl+C to stop...\n'); const monitor = async () => { try { console.clear(); console.log(`šŸ• Last updated: ${new Date().toLocaleTimeString()}\n`); await this.getConnectionsGroupedByName(); } catch (error) { console.error('āŒ Monitoring error:', error); console.log('Retrying in next interval...'); } }; // Initial run await monitor(); // Set up interval const interval = setInterval(monitor, intervalSeconds * 1000); // Handle graceful shutdown process.on('SIGINT', async () => { clearInterval(interval); console.log('\nšŸ‘‹ Monitoring stopped'); await this.redis.disconnect(); process.exit(0); }); } } // Main execution async function main() { const args = process.argv.slice(2); const monitor = new RedisConnectionMonitor(); if (args.includes('--monitor') || args.includes('-m')) { const intervalArg = args.find(arg => arg.startsWith('--interval=')); const interval = intervalArg ? parseInt(intervalArg.split('=')[1]) : 5; await monitor.monitorConnections(interval); } else { await monitor.getConnectionsGroupedByName(); } } // Help text if (process.argv.includes('--help') || process.argv.includes('-h')) { console.log(` šŸ‰ Redis/Dragonfly Connection Monitor Usage: bun run scripts/get-redis-connections.ts [options] Options: --help, -h Show this help message --monitor, -m Monitor connections in real-time --interval=N Set monitoring refresh interval in seconds (default: 5) Examples: bun run scripts/get-redis-connections.ts # One-time connection list bun run scripts/get-redis-connections.ts --monitor # Real-time monitoring bun run scripts/get-redis-connections.ts -m --interval=10 # Monitor every 10 seconds Environment Variables: DRAGONFLY_HOST Dragonfly host (default: localhost) DRAGONFLY_PORT Dragonfly port (default: 6379) DRAGONFLY_PASSWORD Dragonfly password (if auth enabled) DRAGONFLY_DATABASE Database number (default: 0) Features: ✨ Groups connections by clean names (removes timestamp suffixes) šŸ“Š Shows connection statistics and command counts šŸ”„ Real-time monitoring mode available šŸŽØ Color-coded output with connection details `); process.exit(0); } // Run the script console.log('šŸš€ Starting Redis connection monitor...'); main().catch((error) => { console.error('āŒ Script error:', error); process.exit(1); });