Skip to main content
Glama
redisManager.ts8.65 kB
import Redis from "ioredis"; import { logger } from "../utils/logger.js"; export interface RedisInfo { readonly version: string; readonly uptime: number; readonly connectedClients: number; readonly usedMemory: number; readonly usedMemoryPeak: number; readonly memoryFragmentationRatio: number; readonly totalCommandsProcessed: number; readonly opsPerSec: number; readonly role: "master" | "slave"; } export interface MemoryStats { readonly usedMemoryHuman: string; readonly usedMemoryPeakHuman: string; readonly memoryFragmentationRatio: number; readonly memoryAllocator: string; readonly usedMemoryRss: number; readonly maxMemory: number; readonly maxMemoryPolicy: string; } export interface KeyspaceStats { readonly database: number; readonly keys: number; readonly expires: number; readonly avgTtl: number; } export interface SlowLogEntry { readonly id: number; readonly timestamp: number; readonly duration: number; readonly command: string; readonly clientAddress: string; readonly clientName: string; } export interface ClientInfo { readonly id: string; readonly address: string; readonly name: string; readonly age: number; readonly idle: number; readonly db: number; readonly cmd: string; } export interface SaveResult { readonly success: boolean; readonly durationMs: number; readonly lastSave: number; } export class RedisManagerService { private readonly redis: Redis; public constructor(connectionConfig?: { host?: string; port?: number; password?: string; db?: number; retryStrategy?: (times: number) => number; maxRetriesPerRequest?: number; }) { this.redis = new Redis( connectionConfig ?? { host: process.env.REDIS_HOST ?? "localhost", port: Number(process.env.REDIS_PORT ?? 6379), password: process.env.REDIS_PASSWORD, db: Number(process.env.REDIS_DB ?? 0), retryStrategy: (times) => { const delay = Math.min(times * 50, 2000); return delay; }, maxRetriesPerRequest: 3, }, ); this.redis.on("error", (error) => { logger.error("Redis connection error", { error }); }); this.redis.on("connect", () => { logger.info("Redis connected", { host: this.redis.options.host, port: this.redis.options.port, }); }); } public async getInfo(): Promise<RedisInfo> { const info = await this.redis.info(); const parsed = this.parseInfoString(info); return { version: parsed.server?.redis_version ?? "unknown", uptime: Number(parsed.server?.uptime_in_seconds ?? 0), connectedClients: Number(parsed.clients?.connected_clients ?? 0), usedMemory: Number(parsed.memory?.used_memory ?? 0), usedMemoryPeak: Number(parsed.memory?.used_memory_peak ?? 0), memoryFragmentationRatio: Number(parsed.memory?.mem_fragmentation_ratio ?? 1.0), totalCommandsProcessed: Number(parsed.stats?.total_commands_processed ?? 0), opsPerSec: Number(parsed.stats?.instantaneous_ops_per_sec ?? 0), role: (parsed.replication?.role === "slave" ? "slave" : "master") as "master" | "slave", }; } public async getMemoryStats(): Promise<MemoryStats> { const info = await this.redis.info("memory"); const parsed = this.parseInfoString(info); const memory = parsed.memory ?? {}; return { usedMemoryHuman: memory.used_memory_human ?? "0B", usedMemoryPeakHuman: memory.used_memory_peak_human ?? "0B", memoryFragmentationRatio: Number(memory.mem_fragmentation_ratio ?? 1.0), memoryAllocator: memory.mem_allocator ?? "unknown", usedMemoryRss: Number(memory.used_memory_rss ?? 0), maxMemory: Number(memory.maxmemory ?? 0), maxMemoryPolicy: memory.maxmemory_policy ?? "noeviction", }; } public async getKeyspaceStats(): Promise<KeyspaceStats[]> { const info = await this.redis.info("keyspace"); const parsed = this.parseInfoString(info); const keyspace = parsed.keyspace ?? {}; const stats: KeyspaceStats[] = []; for (const [key, value] of Object.entries(keyspace)) { const dbMatch = key.match(/^db(\d+)$/); if (!dbMatch) { continue; } const dbNumber = Number(dbMatch[1]); const valueStr = String(value); const keysMatch = valueStr.match(/keys=(\d+)/); const expiresMatch = valueStr.match(/expires=(\d+)/); const avgTtlMatch = valueStr.match(/avg_ttl=(\d+)/); stats.push({ database: dbNumber, keys: keysMatch ? Number(keysMatch[1]) : 0, expires: expiresMatch ? Number(expiresMatch[1]) : 0, avgTtl: avgTtlMatch ? Number(avgTtlMatch[1]) : 0, }); } return stats; } public async getSlowLog(count: number = 10): Promise<SlowLogEntry[]> { const logs = await this.redis.slowlog("GET", count) as unknown[]; const entries: SlowLogEntry[] = []; for (const log of logs) { if (!Array.isArray(log) || log.length < 6) { continue; } const [id, timestamp, duration, command, clientAddress, clientName] = log; entries.push({ id: Number(id), timestamp: Number(timestamp), duration: Number(duration), command: Array.isArray(command) ? command.join(" ") : String(command), clientAddress: String(clientAddress), clientName: String(clientName), }); } return entries; } public async getClientList(): Promise<ClientInfo[]> { const clientList = await this.redis.client("LIST") as unknown as string; const lines = clientList.split("\n").filter((line: string) => line.trim()); const clients: ClientInfo[] = []; for (const line of lines) { const parts = line.split(" "); const clientData: Record<string, string> = {}; for (const part of parts) { const [key, value] = part.split("="); if (key && value) { clientData[key] = value; } } clients.push({ id: clientData.id ?? "unknown", address: clientData.addr ?? "unknown", name: clientData.name ?? "", age: Number(clientData.age ?? 0), idle: Number(clientData.idle ?? 0), db: Number(clientData.db ?? 0), cmd: clientData.cmd ?? "unknown", }); } return clients; } public async flushDatabase(database?: number): Promise<void> { if (database !== undefined) { await this.redis.select(database); await this.redis.flushdb(); logger.warn("Redis database flushed", { database }); } else { await this.redis.flushdb(); logger.warn("Current Redis database flushed"); } } public async bgsave(): Promise<SaveResult> { const startTime = Date.now(); try { await this.redis.bgsave(); const durationMs = Date.now() - startTime; // Get last save time const lastSave = await this.redis.lastsave(); logger.info("Redis BGSAVE initiated", { durationMs, lastSave }); return { success: true, durationMs, lastSave: Number(lastSave), }; } catch (error) { const durationMs = Date.now() - startTime; logger.error("Redis BGSAVE failed", { error, durationMs }); return { success: false, durationMs, lastSave: 0, }; } } public async ping(): Promise<boolean> { try { const result = await this.redis.ping(); return result === "PONG"; } catch { return false; } } public async close(): Promise<void> { await this.redis.quit(); logger.info("Redis connection closed"); } private parseInfoString(info: string): Record<string, Record<string, string>> { const sections: Record<string, Record<string, string>> = {}; let currentSection = "general"; sections[currentSection] = {}; const lines = info.split("\n"); for (const line of lines) { const trimmed = line.trim(); // Skip empty lines and comments if (!trimmed || trimmed.startsWith("#")) { // Check if it's a section header const sectionMatch = trimmed.match(/^#\s*(\w+)$/); if (sectionMatch) { currentSection = sectionMatch[1].toLowerCase(); sections[currentSection] = {}; } continue; } // Parse key:value pairs const colonIndex = trimmed.indexOf(":"); if (colonIndex > 0) { const key = trimmed.slice(0, colonIndex).trim(); const value = trimmed.slice(colonIndex + 1).trim(); sections[currentSection][key] = value; } } return sections; } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/acampkin95/MCPCentralManager'

If you have feedback or need assistance with the MCP directory API, please join our Discord server