- 4个数据源菜单(MySQL/PG/Redis/Mongo)合并为1个「数据源」 - InstancePage 改为统一入口,顶部Tab切换类型过滤 - 添加数据源改为弹框Modal(默认MySQL) - 去掉左下角用户区,退出登录移到右上角header - 侧边栏收窄220px,整体布局更紧凑 - 后端202 tests passing - 前端构建零错误
270 lines
9.0 KiB
Python
270 lines
9.0 KiB
Python
"""Redis connector — async interface for Redis instance management.
|
|
|
|
Provides connection, info retrieval, key CRUD, command execution, and
|
|
performance monitoring via redis.asyncio.
|
|
"""
|
|
import json
|
|
from typing import Any, Dict, List, Optional, Tuple
|
|
|
|
import redis.asyncio as aioredis
|
|
|
|
from app.core.crypto import decrypt_password
|
|
|
|
|
|
class RedisConnector:
|
|
"""Async Redis connector for instance operations."""
|
|
|
|
@staticmethod
|
|
async def get_connection(instance) -> aioredis.Redis:
|
|
"""Create an async Redis connection from an Instance model."""
|
|
pwd = decrypt_password(instance.password)
|
|
return aioredis.Redis(
|
|
host=instance.host,
|
|
port=instance.port,
|
|
password=pwd or None,
|
|
decode_responses=True,
|
|
)
|
|
|
|
@staticmethod
|
|
async def get_info(instance) -> dict:
|
|
"""Get Redis INFO ALL parsed into a structured dict."""
|
|
conn = await RedisConnector.get_connection(instance)
|
|
try:
|
|
raw: str = await conn.info("all")
|
|
return raw
|
|
finally:
|
|
await conn.close()
|
|
|
|
@staticmethod
|
|
async def get_dbsize(instance, db: int = 0) -> int:
|
|
"""Get number of keys in a database."""
|
|
conn = await RedisConnector.get_connection(instance)
|
|
try:
|
|
await conn.select(db)
|
|
return await conn.dbsize()
|
|
finally:
|
|
await conn.close()
|
|
|
|
@staticmethod
|
|
async def scan_keys(
|
|
instance,
|
|
db: int = 0,
|
|
pattern: str = "*",
|
|
type_filter: Optional[str] = None,
|
|
cursor: int = 0,
|
|
count: int = 50,
|
|
) -> Tuple[int, List[Dict[str, Any]]]:
|
|
"""SCAN keys with optional type filter. Returns (next_cursor, key_details)."""
|
|
conn = await RedisConnector.get_connection(instance)
|
|
try:
|
|
await conn.select(db)
|
|
next_cursor, keys = await conn.scan(
|
|
cursor=cursor, match=pattern, count=count
|
|
)
|
|
result = []
|
|
for key in keys:
|
|
key_type = await conn.type(key)
|
|
if type_filter and key_type != type_filter:
|
|
continue
|
|
ttl = await conn.ttl(key)
|
|
result.append(
|
|
{"key": key, "type": key_type, "ttl": ttl}
|
|
)
|
|
return next_cursor, result
|
|
finally:
|
|
await conn.close()
|
|
|
|
@staticmethod
|
|
async def get_key_value(
|
|
instance, db: int, key: str, key_type: Optional[str] = None
|
|
) -> Any:
|
|
"""Get value for a key based on its type."""
|
|
conn = await RedisConnector.get_connection(instance)
|
|
try:
|
|
await conn.select(db)
|
|
if key_type is None:
|
|
key_type = await conn.type(key)
|
|
|
|
if key_type == "string":
|
|
value = await conn.get(key)
|
|
elif key_type == "hash":
|
|
value = await conn.hgetall(key)
|
|
elif key_type == "list":
|
|
value = await conn.lrange(key, 0, -1)
|
|
elif key_type == "set":
|
|
value = list(await conn.smembers(key))
|
|
elif key_type == "zset":
|
|
raw = await conn.zrange(key, 0, -1, withscores=True)
|
|
value = [{"member": m, "score": s} for m, s in raw]
|
|
else:
|
|
value = None
|
|
|
|
ttl = await conn.ttl(key)
|
|
return {"key": key, "type": key_type, "value": value, "ttl": ttl}
|
|
finally:
|
|
await conn.close()
|
|
|
|
@staticmethod
|
|
async def set_key_value(
|
|
instance,
|
|
db: int,
|
|
key: str,
|
|
value: Any,
|
|
type_: str = "string",
|
|
ttl: Optional[int] = None,
|
|
) -> str:
|
|
"""Set a key value. Type determines how value is stored."""
|
|
conn = await RedisConnector.get_connection(instance)
|
|
try:
|
|
await conn.select(db)
|
|
|
|
if type_ == "string":
|
|
await conn.set(key, value)
|
|
elif type_ == "hash":
|
|
# value should be a dict
|
|
if isinstance(value, str):
|
|
value = json.loads(value)
|
|
await conn.hset(key, mapping=value)
|
|
elif type_ == "list":
|
|
if isinstance(value, str):
|
|
value = json.loads(value)
|
|
await conn.delete(key)
|
|
if value:
|
|
await conn.rpush(key, *value)
|
|
elif type_ == "set":
|
|
if isinstance(value, str):
|
|
value = json.loads(value)
|
|
await conn.delete(key)
|
|
if value:
|
|
await conn.sadd(key, *value)
|
|
elif type_ == "zset":
|
|
if isinstance(value, str):
|
|
value = json.loads(value)
|
|
await conn.delete(key)
|
|
if isinstance(value, list):
|
|
for item in value:
|
|
member = item.get("member", "")
|
|
score = item.get("score", 0)
|
|
await conn.zadd(key, {member: score})
|
|
|
|
if ttl and ttl > 0:
|
|
await conn.expire(key, ttl)
|
|
|
|
return "OK"
|
|
finally:
|
|
await conn.close()
|
|
|
|
@staticmethod
|
|
async def del_key(instance, db: int, key: str) -> int:
|
|
"""Delete a key. Returns number of keys deleted."""
|
|
conn = await RedisConnector.get_connection(instance)
|
|
try:
|
|
await conn.select(db)
|
|
return await conn.delete(key)
|
|
finally:
|
|
await conn.close()
|
|
|
|
@staticmethod
|
|
async def set_ttl(instance, db: int, key: str, seconds: int) -> bool:
|
|
"""Set TTL on a key."""
|
|
conn = await RedisConnector.get_connection(instance)
|
|
try:
|
|
await conn.select(db)
|
|
return await conn.expire(key, seconds)
|
|
finally:
|
|
await conn.close()
|
|
|
|
@staticmethod
|
|
async def get_ttl(instance, db: int, key: str) -> int:
|
|
"""Get TTL of a key."""
|
|
conn = await RedisConnector.get_connection(instance)
|
|
try:
|
|
await conn.select(db)
|
|
return await conn.ttl(key)
|
|
finally:
|
|
await conn.close()
|
|
|
|
@staticmethod
|
|
async def execute_command(instance, db: int, command: str) -> str:
|
|
"""Execute a raw Redis command."""
|
|
conn = await RedisConnector.get_connection(instance)
|
|
try:
|
|
await conn.select(db)
|
|
parts = command.strip().split()
|
|
if not parts:
|
|
return ""
|
|
cmd = parts[0].upper()
|
|
args = parts[1:]
|
|
result = await conn.execute_command(cmd, *args)
|
|
if result is None:
|
|
return "(nil)"
|
|
if isinstance(result, bytes):
|
|
return result.decode("utf-8", errors="replace")
|
|
if isinstance(result, (list, set)):
|
|
formatted = []
|
|
for item in result:
|
|
if isinstance(item, bytes):
|
|
formatted.append(item.decode("utf-8", errors="replace"))
|
|
else:
|
|
formatted.append(str(item))
|
|
return "\n".join(formatted)
|
|
return str(result)
|
|
finally:
|
|
await conn.close()
|
|
|
|
@staticmethod
|
|
async def flush_db(instance, db: int = 0) -> str:
|
|
"""FLUSHDB — dangerous! Admin only."""
|
|
conn = await RedisConnector.get_connection(instance)
|
|
try:
|
|
await conn.select(db)
|
|
await conn.flushdb()
|
|
return "OK"
|
|
finally:
|
|
await conn.close()
|
|
|
|
@staticmethod
|
|
async def get_slowlog(instance, count: int = 10) -> List[dict]:
|
|
"""Get slowlog entries."""
|
|
conn = await RedisConnector.get_connection(instance)
|
|
try:
|
|
raw = await conn.slowlog_get(count)
|
|
result = []
|
|
for entry in raw:
|
|
result.append(
|
|
{
|
|
"id": entry.get("id", entry.get(0)),
|
|
"timestamp": entry.get("start_time", entry.get(1)),
|
|
"duration_us": entry.get("duration", entry.get(2)),
|
|
"command": " ".join(
|
|
str(c) if not isinstance(c, bytes) else c.decode("utf-8", errors="replace")
|
|
for c in entry.get("command", entry.get(3, []))
|
|
),
|
|
}
|
|
)
|
|
return result
|
|
finally:
|
|
await conn.close()
|
|
|
|
@staticmethod
|
|
async def get_clients(instance) -> List[dict]:
|
|
"""Get client list."""
|
|
conn = await RedisConnector.get_connection(instance)
|
|
try:
|
|
raw: str = await conn.client_list()
|
|
clients = []
|
|
for line in raw.split("\n"):
|
|
line = line.strip()
|
|
if not line:
|
|
continue
|
|
client = {}
|
|
for pair in line.split(" "):
|
|
if "=" in pair:
|
|
k, v = pair.split("=", 1)
|
|
client[k] = v
|
|
if client:
|
|
clients.append(client)
|
|
return clients
|
|
finally:
|
|
await conn.close()
|