From 77323715e700265a2d5535bd3a2e9ff5e14c55eb Mon Sep 17 00:00:00 2001 From: zipg Date: Thu, 10 Sep 2026 19:23:18 +0800 Subject: [PATCH] feat(redis): support MONITOR live command log --- .../src/components/layout/ContentArea.vue | 7 +- apps/desktop/src/i18n/locales/en.ts | 2 + apps/desktop/src/i18n/locales/es.ts | 2 + apps/desktop/src/i18n/locales/it.ts | 2 + apps/desktop/src/i18n/locales/ja.ts | 2 + apps/desktop/src/i18n/locales/ko.ts | 2 + apps/desktop/src/i18n/locales/pt-BR.ts | 2 + apps/desktop/src/i18n/locales/zh-CN.ts | 2 + apps/desktop/src/i18n/locales/zh-TW.ts | 2 + .../__tests__/app/openTabsPersistence.spec.ts | 9 ++ .../lib/__tests__/redis/redisMonitor.spec.ts | 102 ++++++++++++++++++ .../src/lib/app/openTabsPersistence.ts | 1 + apps/desktop/src/lib/backend/http.ts | 4 +- apps/desktop/src/lib/backend/tauri.ts | 4 +- .../src/lib/redis/redisCommandSafety.ts | 3 + apps/desktop/src/lib/redis/redisMonitor.ts | 66 ++++++++++++ apps/desktop/src/stores/queryStore.ts | 46 ++++++++ apps/desktop/src/types/database.ts | 1 + crates/dbx-core/src/db/redis_driver.rs | 34 ++++++ crates/dbx-core/src/redis_ops.rs | 22 ++++ crates/dbx-web/src/routes/redis_pubsub_ws.rs | 58 +++++++++- docs/content/docs/redis.cn.mdx | 10 ++ docs/content/docs/redis.mdx | 10 ++ docs/public/screenshots/redis-monitor.png | Bin 0 -> 72006 bytes src-tauri/src/commands/redis_pubsub_server.rs | 53 ++++++++- 25 files changed, 439 insertions(+), 7 deletions(-) create mode 100644 apps/desktop/src/lib/__tests__/redis/redisMonitor.spec.ts create mode 100644 apps/desktop/src/lib/redis/redisMonitor.ts create mode 100644 docs/public/screenshots/redis-monitor.png diff --git a/apps/desktop/src/components/layout/ContentArea.vue b/apps/desktop/src/components/layout/ContentArea.vue index 8e8cf7758d..10428ece01 100644 --- a/apps/desktop/src/components/layout/ContentArea.vue +++ b/apps/desktop/src/components/layout/ContentArea.vue @@ -483,7 +483,7 @@ const resultArchiveExporting = ref(false); const canExportResultArchive = computed(() => props.activeTab.mode === "query" && (!!props.activeTab.result || !!props.activeTab.results?.length || !!props.activeTab.resultRuns?.length)); const resultAutoSave = computed(() => props.activeTab.resultAutoSave === true); const activeResultRunItem = computed(() => resultRuns.value.find((run) => run.active)); -const activeResultIsLoading = computed(() => isActiveResultLoading(props.activeTab)); +const activeResultIsLoading = computed(() => !props.activeTab.redisMonitorActive && isActiveResultLoading(props.activeTab)); const showResultRunTabs = computed(() => resultRuns.value.length > 0 && resultRunDisplayMode.value === "tabs"); const showResultRunSelector = computed(() => resultRuns.value.length > 0 && resultRunDisplayMode.value === "list"); const canCloseQueryResult = computed(() => props.activeTab.mode === "query" && !props.activeTab.isExecuting && !props.activeTab.activeResultRunId && (!!props.activeTab.result || !!props.activeTab.results?.length || props.activeTab.resultEvicted === true)); @@ -1717,6 +1717,11 @@ defineExpose({ /> +
+ {{ t("redis.monitorListening") }} + +
+ { expect(restored.isExecuting).toBe(false); }); + it("does not resume a MONITOR stream when restoring tabs", () => { + const tab = queryTab({ sql: "MONITOR", redisMonitorActive: true, isExecuting: true }); + const [saved] = serializeOpenTabs([tab]); + const [restored] = roundTrip([tab]); + expect(saved).not.toHaveProperty("redisMonitorActive"); + expect(restored.redisMonitorActive).toBe(false); + expect(restored.isExecuting).toBe(false); + }); + it("preserves an external Doris catalog across tab restore", () => { const [restored] = roundTrip([queryTab({ database: "dbx_catalog_completion", catalog: "dbx_mysql_catalog" })]); diff --git a/apps/desktop/src/lib/__tests__/redis/redisMonitor.spec.ts b/apps/desktop/src/lib/__tests__/redis/redisMonitor.spec.ts new file mode 100644 index 0000000000..c0bb603ea6 --- /dev/null +++ b/apps/desktop/src/lib/__tests__/redis/redisMonitor.spec.ts @@ -0,0 +1,102 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import { REDIS_MONITOR_MAX_ROWS, startRedisMonitor } from "../../redis/redisMonitor"; +import { classifyRedisCommandSafety } from "../../redis/redisCommandSafety"; + +function createSocket() { + return { close: vi.fn(), onmessage: null, onerror: null, onclose: null } as unknown as WebSocket; +} + +function receive(socket: WebSocket, data: object) { + socket.onmessage?.(new MessageEvent("message", { data: JSON.stringify(data) })); +} + +afterEach(() => vi.useRealTimers()); + +describe("Redis MONITOR stream", () => { + it("allows standalone MONITOR without permitting arbitrary commands", () => { + expect(classifyRedisCommandSafety("monitor")).toBe("allowed"); + expect(classifyRedisCommandSafety(" MONITOR; ")).toBe("allowed"); + expect(classifyRedisCommandSafety("MONITOR extra")).toBe("blocked"); + expect(classifyRedisCommandSafety("MONITOR; FLUSHALL")).toBe("blocked"); + }); + it("streams successive batches and bounds retained rows", async () => { + vi.useFakeTimers(); + const socket = createSocket(); + const onRows = vi.fn(); + const monitor = startRedisMonitor(async () => socket, onRows); + await Promise.resolve(); + receive(socket, { ready: true }); + expect(onRows).toHaveBeenLastCalledWith([]); + receive(socket, { message: "first" }); + vi.advanceTimersByTime(100); + expect(onRows).toHaveBeenLastCalledWith(["first"]); + for (let index = 0; index < REDIS_MONITOR_MAX_ROWS + 10; index++) receive(socket, { message: String(index) }); + vi.advanceTimersByTime(100); + const rows = onRows.mock.lastCall![0]; + expect(rows).toHaveLength(REDIS_MONITOR_MAX_ROWS); + expect(rows[0]).toBe("10"); + monitor.stop(); + await monitor.done; + expect(socket.close).toHaveBeenCalledOnce(); + expect(vi.getTimerCount()).toBe(0); + }); + + it("closes a socket that arrives after cancellation", async () => { + const socket = createSocket(); + let resolveConnection!: (socket: WebSocket) => void; + const monitor = startRedisMonitor( + () => + new Promise((resolve) => { + resolveConnection = resolve; + }), + vi.fn(), + ); + monitor.stop(); + resolveConnection(socket); + await monitor.done; + expect(socket.close).toHaveBeenCalledOnce(); + }); + + it("reports server errors and cleans up", async () => { + const socket = createSocket(); + const monitor = startRedisMonitor(async () => socket, vi.fn()); + const failure = expect(monitor.done).rejects.toThrow("NOPERM"); + await Promise.resolve(); + receive(socket, { error: "NOPERM" }); + await failure; + expect(socket.close).toHaveBeenCalledOnce(); + }); + + it("reports unexpected disconnects and ignores events after stop", async () => { + vi.useFakeTimers(); + const socket = createSocket(); + const onRows = vi.fn(); + const monitor = startRedisMonitor(async () => socket, onRows); + const failure = expect(monitor.done).rejects.toThrow("connection closed"); + await Promise.resolve(); + receive(socket, { ready: true }); + socket.onclose?.(new Event("close") as CloseEvent); + await failure; + receive(socket, { message: "late" }); + vi.advanceTimersByTime(1000); + expect(onRows).toHaveBeenCalledTimes(1); + expect(vi.getTimerCount()).toBe(0); + }); + + it("times out setup without timing out an idle ready stream", async () => { + vi.useFakeTimers(); + const socket = createSocket(); + const monitor = startRedisMonitor(async () => socket, vi.fn()); + await Promise.resolve(); + receive(socket, { ready: true }); + vi.advanceTimersByTime(60000); + expect(socket.close).not.toHaveBeenCalled(); + monitor.stop(); + await monitor.done; + const pending = startRedisMonitor(() => new Promise(() => {}), vi.fn()); + const failure = expect(pending.done).rejects.toThrow("timed out"); + vi.advanceTimersByTime(30000); + await failure; + expect(vi.getTimerCount()).toBe(0); + }); +}); diff --git a/apps/desktop/src/lib/app/openTabsPersistence.ts b/apps/desktop/src/lib/app/openTabsPersistence.ts index 33a8d1ae5b..46c54dee4c 100644 --- a/apps/desktop/src/lib/app/openTabsPersistence.ts +++ b/apps/desktop/src/lib/app/openTabsPersistence.ts @@ -233,6 +233,7 @@ function restoreOpenTabsArray(parsed: unknown, rawActiveTabId: string | null, op mode, sql: typeof tab.sql === "string" ? tab.sql : "", isExecuting: false, + redisMonitorActive: false, isCancelling: false, queryExecutionStartedAt: undefined, executingResultRunId: undefined, diff --git a/apps/desktop/src/lib/backend/http.ts b/apps/desktop/src/lib/backend/http.ts index 278b21b322..f6bba47bd3 100644 --- a/apps/desktop/src/lib/backend/http.ts +++ b/apps/desktop/src/lib/backend/http.ts @@ -2989,8 +2989,8 @@ export async function redisPubSubPublish(connectionId: string, db: number, chann }); } -export async function redisPubSubConnect(connectionId: string): Promise { - return new WebSocket(apiWebSocketUrl(`/api/redis/pubsub/ws?connectionId=${encodeURIComponent(connectionId)}`)); +export async function redisPubSubConnect(connectionId: string, monitor = false): Promise { + return new WebSocket(apiWebSocketUrl(`/api/redis/pubsub/ws?connectionId=${encodeURIComponent(connectionId)}&monitor=${monitor}`)); } export async function redisSlowlogGet(connectionId: string, count: number, nodeHost?: string, nodePort?: number): Promise { diff --git a/apps/desktop/src/lib/backend/tauri.ts b/apps/desktop/src/lib/backend/tauri.ts index f9e696df98..9495e9294b 100644 --- a/apps/desktop/src/lib/backend/tauri.ts +++ b/apps/desktop/src/lib/backend/tauri.ts @@ -2926,9 +2926,9 @@ export async function redisPubSubPublish(connectionId: string, db: number, chann return invoke("redis_pubsub_publish", { connectionId, db, channel, message }); } -export async function redisPubSubConnect(connectionId: string): Promise { +export async function redisPubSubConnect(connectionId: string, monitor = false): Promise { const port = await invoke("redis_pubsub_server_port"); - return new WebSocket(`ws://127.0.0.1:${port}/api/redis/pubsub/ws?connectionId=${encodeURIComponent(connectionId)}`); + return new WebSocket(`ws://127.0.0.1:${port}/api/redis/pubsub/ws?connectionId=${encodeURIComponent(connectionId)}&monitor=${monitor}`); } export async function redisSlowlogGet(connectionId: string, count: number, nodeHost?: string, nodePort?: number): Promise { diff --git a/apps/desktop/src/lib/redis/redisCommandSafety.ts b/apps/desktop/src/lib/redis/redisCommandSafety.ts index 2ef9aab814..67fc907632 100644 --- a/apps/desktop/src/lib/redis/redisCommandSafety.ts +++ b/apps/desktop/src/lib/redis/redisCommandSafety.ts @@ -1,3 +1,5 @@ +import { isRedisMonitorCommand } from "./redisMonitor"; + export type RedisCommandSafety = "allowed" | "write" | "confirm" | "blocked"; const BLOCKED_COMMANDS = new Set(["KEYS", "FLUSHALL", "SHUTDOWN", "CONFIG", "SAVE", "BGSAVE", "SLAVEOF", "REPLICAOF", "MIGRATE", "MODULE", "SCRIPT", "EVAL", "EVALSHA"]); @@ -273,6 +275,7 @@ export function firstRedisCommandToken(command: string): string { } export function classifyRedisCommandSafety(command: string): RedisCommandSafety { + if (isRedisMonitorCommand(command)) return "allowed"; const token = firstRedisCommandToken(command); if (BLOCKED_COMMANDS.has(token)) return "blocked"; if (CONFIRM_COMMANDS.has(token)) return "confirm"; diff --git a/apps/desktop/src/lib/redis/redisMonitor.ts b/apps/desktop/src/lib/redis/redisMonitor.ts new file mode 100644 index 0000000000..83d49d451a --- /dev/null +++ b/apps/desktop/src/lib/redis/redisMonitor.ts @@ -0,0 +1,66 @@ +export const REDIS_MONITOR_MAX_ROWS = 1000; + +export function isRedisMonitorCommand(command: string): boolean { + return /^MONITOR\s*;?$/i.test(command.trim()); +} + +export function startRedisMonitor(connect: () => Promise, onRows: (rows: string[]) => void) { + let socket: WebSocket | undefined; + let finished = false; + let rows: string[] = []; + let dirty = false; + let resolveDone!: () => void; + let rejectDone!: (error: Error) => void; + const done = new Promise((resolve, reject) => { + resolveDone = resolve; + rejectDone = reject; + }); + const flush = () => { + if (dirty) { + dirty = false; + onRows(rows.slice()); + } + }; + const timer = setInterval(flush, 100); + const timeout = setTimeout(() => finish(new Error("Redis MONITOR connection timed out")), 30000); + function finish(error?: Error) { + if (finished) return; + finished = true; + clearInterval(timer); + clearTimeout(timeout); + flush(); + socket?.close(); + if (error) rejectDone(error); + else resolveDone(); + } + void connect() + .then((connected) => { + socket = connected; + if (finished) { + socket.close(); + return; + } + socket.onmessage = (event) => { + if (finished) return; + try { + const data = JSON.parse(event.data); + if (typeof data.error === "string") { + finish(new Error(data.error)); + } else if (data.ready === true) { + clearTimeout(timeout); + onRows([]); + } else if (typeof data.message === "string") { + rows.push(data.message.slice(0, 16384)); + if (rows.length > REDIS_MONITOR_MAX_ROWS) rows = rows.slice(-REDIS_MONITOR_MAX_ROWS); + dirty = true; + } + } catch { + finish(new Error("Invalid Redis MONITOR response")); + } + }; + socket.onerror = () => finish(new Error("Redis MONITOR connection failed")); + socket.onclose = () => finish(new Error("Redis MONITOR connection closed")); + }) + .catch((error) => finish(error instanceof Error ? error : new Error(String(error)))); + return { done, stop: () => finish() }; +} diff --git a/apps/desktop/src/stores/queryStore.ts b/apps/desktop/src/stores/queryStore.ts index 9abf4431c3..cc9b3c605c 100644 --- a/apps/desktop/src/stores/queryStore.ts +++ b/apps/desktop/src/stores/queryStore.ts @@ -1,5 +1,6 @@ import { UPDATE_RESTORE_KEY, assertUpdateAllowsInteraction } from "@/lib/app/updatePreparation"; import { defineStore } from "pinia"; +import { isRedisMonitorCommand, startRedisMonitor } from "@/lib/redis/redisMonitor"; import { uuid } from "@/lib/common/utils"; import { computed, markRaw, nextTick, onScopeDispose, reactive, ref, watch } from "vue"; import { useI18n } from "vue-i18n"; @@ -789,6 +790,7 @@ function getI18nT() { } export const useQueryStore = defineStore("query", () => { + const redisMonitors = new Map void>(); const t = getI18nT(); const settingsStore = useSettingsStore(); const tabs = ref([]); @@ -5769,6 +5771,43 @@ export const useQueryStore = defineStore("query", () => { .map((line) => line.trim()) .filter((line) => line.length > 0); if (commands.length === 0) return false; + if (commands.length === 1 && isRedisMonitorCommand(commands[0])) { + const monitor = startRedisMonitor( + () => api.redisPubSubConnect(executionConnectionId, true), + (rows) => { + const current = findExecutionTab(id); + if (current?.executionId !== executionId) return; + current.result = markQueryResultRowsRaw(annotateQueryResultSource({ columns: ["MONITOR"], rows: rows.map((message) => [message]), affected_rows: 0, execution_time_ms: performance.now() - startedAt }, "MONITOR")); + current.results = undefined; + current.activeResultIndex = undefined; + current.queryEditabilityReason = undefined; + current.queryWriteTargets = undefined; + current.tableMeta = undefined; + current.resultBaseSql = "MONITOR"; + current.redisMonitorActive = true; + current.queryAnalysis = undefined; + current.querySourceColumns = undefined; + current.resultColumnComments = undefined; + current.queryDisplaySourceColumns = undefined; + current.mongoEditTarget = undefined; + if (!producedResult) { + publishResultGeneration(current, "execute"); + syncDisplayedResultRun(current, "MONITOR", captureResultRun); + } + producedResult = true; + touchResult(current); + }, + ); + redisMonitors.set(executionId, monitor.stop); + try { + await monitor.done; + } finally { + redisMonitors.delete(executionId); + const current = findExecutionTab(id); + if (current?.executionId === executionId) current.redisMonitorActive = false; + } + return producedResult; + } queryExecutionLog("info", "redis:start", { traceId, db: currentDb, commandCount: commands.length, sqlLength: sql.length }); const allResults: QueryResult[] = []; @@ -7205,6 +7244,13 @@ export const useQueryStore = defineStore("query", () => { const executionId = tab.executionId; if (!executionId) return false; + const stopMonitor = redisMonitors.get(executionId); + if (stopMonitor) { + tab.isCancelling = true; + tab.cancelRequestCount = (tab.cancelRequestCount ?? 0) + 1; + stopMonitor(); + return true; + } tab.isCancelling = true; // 单调递增、不随取消结果回退:导航流程据此判断"执行期间用户请求过停止" // (isCancelling 在取消失败或查询先完成时会被清掉,无法承担这个语义) diff --git a/apps/desktop/src/types/database.ts b/apps/desktop/src/types/database.ts index 373f724198..92129448ec 100644 --- a/apps/desktop/src/types/database.ts +++ b/apps/desktop/src/types/database.ts @@ -1224,6 +1224,7 @@ export interface QueryTab { explainTableSql?: string; lastExplainedSql?: string; isExecuting: boolean; + redisMonitorActive?: boolean; isCancelling?: boolean; queryExecutionStartedAt?: number; /** Ephemeral per-statement progress for the latest multi-statement execution. */ diff --git a/crates/dbx-core/src/db/redis_driver.rs b/crates/dbx-core/src/db/redis_driver.rs index a847fa361c..45efb84262 100644 --- a/crates/dbx-core/src/db/redis_driver.rs +++ b/crates/dbx-core/src/db/redis_driver.rs @@ -1121,6 +1121,35 @@ pub async fn connect_direct_node( connect_client(client).await } +pub async fn connect_monitor( + config: &ConnectionConfig, + host: &str, + port: u16, + timeout: std::time::Duration, +) -> Result { + let mut last_error = String::new(); + for info in standalone_connection_infos(config, host, port) { + let client = redis::Client::open(info).map_err(|error| error.to_string())?; + let connect = async { + let mut validation = client.get_multiplexed_async_connection().await?; + redis::cmd("MONITOR").query_async::<()>(&mut validation).await?; + drop(validation); + client.get_async_monitor().await + }; + match tokio::time::timeout(timeout, connect).await { + Ok(Ok(monitor)) => return Ok(monitor), + Ok(Err(error)) => { + last_error = error.to_string(); + if !is_redis_auth_error(&last_error) { + break; + } + } + Err(_) => return Err("Redis MONITOR connection timed out".to_string()), + } + } + Err(last_error) +} + pub async fn connect_pubsub( config: &ConnectionConfig, host: &str, @@ -2218,6 +2247,11 @@ where { let argv = parse_command_argv(command_text)?; let command = argv[0].to_ascii_uppercase(); + if command == "MONITOR" { + return Err( + "MONITOR requires a dedicated streaming connection; run MONITOR alone in the query editor".to_string() + ); + } let safety = classify_command(&command); if !skip_safety_check && safety == RedisCommandSafety::Blocked { return Err(format!("Redis command is blocked for safety: {command}")); diff --git a/crates/dbx-core/src/redis_ops.rs b/crates/dbx-core/src/redis_ops.rs index 2de097c73b..c4bb3e46ef 100644 --- a/crates/dbx-core/src/redis_ops.rs +++ b/crates/dbx-core/src/redis_ops.rs @@ -1141,6 +1141,28 @@ pub async fn redis_publish_core( } } +pub async fn redis_create_monitor_core(state: &AppState, connection_id: &str) -> Result { + let config = state.configs.read().await.get(connection_id).ok_or("Connection config not found")?.clone(); + if config.db_type != crate::models::connection::DatabaseType::Redis { + return Err("Not a Redis connection".to_string()); + } + if config.uses_redis_sentinel() || config.uses_redis_cluster() { + return Err( + "MONITOR requires a direct Redis node connection; connect to the node you want to monitor".to_string() + ); + } + let (host, port) = state.connection_host_port(connection_id, &config).await?; + let mut runtime_config = config.clone(); + state.apply_session_credential(&config, &mut runtime_config, connection_id); + redis_driver::connect_monitor( + &runtime_config, + &host, + port, + std::time::Duration::from_secs(config.effective_connect_timeout_secs()), + ) + .await +} + pub async fn redis_create_pubsub_core(state: &AppState, connection_id: &str) -> Result { let configs = state.configs.read().await; let config = configs.get(connection_id).ok_or("Connection config not found")?.clone(); diff --git a/crates/dbx-web/src/routes/redis_pubsub_ws.rs b/crates/dbx-web/src/routes/redis_pubsub_ws.rs index 22e13260db..da0388361a 100644 --- a/crates/dbx-web/src/routes/redis_pubsub_ws.rs +++ b/crates/dbx-web/src/routes/redis_pubsub_ws.rs @@ -13,6 +13,8 @@ use crate::state::WebState; #[serde(rename_all = "camelCase")] pub struct PubSubWsParams { pub connection_id: String, + #[serde(default)] + monitor: bool, } pub async fn ws_handler( @@ -21,7 +23,61 @@ pub async fn ws_handler( State(state): State>, ) -> impl IntoResponse { let connection_id = params.connection_id; - ws.on_upgrade(move |socket| handle_pubsub_socket(socket, state, connection_id)) + let owner = dbx_core::session_credentials::current_credential_owner(); + ws.on_upgrade(move |socket| async move { + if params.monitor { + dbx_core::session_credentials::with_credential_owner( + owner, + handle_monitor_socket(socket, state, connection_id), + ) + .await; + } else { + handle_pubsub_socket(socket, state, connection_id).await; + } + }) +} + +async fn handle_monitor_socket(mut socket: WebSocket, state: Arc, connection_id: String) { + let monitor = tokio::select! { + result = dbx_core::redis_ops::redis_create_monitor_core(&state.app, &connection_id) => result, + _ = socket.recv() => return, + }; + let monitor = match monitor { + Ok(monitor) => monitor, + Err(error) => { + let _ = socket.send(Message::Text(serde_json::json!({ "error": error }).to_string().into())).await; + return; + } + }; + if socket.send(Message::Text(serde_json::json!({ "ready": true }).to_string().into())).await.is_err() { + return; + } + let mut messages = monitor.into_on_message::(); + let (mut sender, mut receiver) = socket.split(); + loop { + tokio::select! { + incoming = receiver.next() => { + match incoming { + Some(Ok(Message::Ping(payload))) => { + if sender.send(Message::Pong(payload)).await.is_err() { break; } + } + Some(Ok(Message::Pong(_))) => {} + _ => break, + } + } + message = messages.next() => { + let Some(message) = message else { + let _ = sender.send(Message::Text(serde_json::json!({ "error": "Redis MONITOR connection closed" }).to_string().into())).await; + break; + }; + let event = Message::Text(serde_json::json!({ "message": message }).to_string().into()); + match tokio::time::timeout(std::time::Duration::from_secs(5), sender.send(event)).await { + Ok(Ok(())) => {} + _ => break, + } + } + } + } } async fn handle_pubsub_socket(socket: WebSocket, state: Arc, connection_id: String) { diff --git a/docs/content/docs/redis.cn.mdx b/docs/content/docs/redis.cn.mdx index 3d9a5f5fd9..16eedfb3b6 100644 --- a/docs/content/docs/redis.cn.mdx +++ b/docs/content/docs/redis.cn.mdx @@ -114,6 +114,16 @@ Key 统计和 Slowlog 会额外调用 Redis 命令。大型实例或集群中应 清空 DB、批量删除和 `FLUSH*` 无法撤销。只读连接会拒绝写入;生产环境保护和危险命令规则会继续生效。 +## 实时 MONITOR + +![Redis MONITOR 实时日志](/screenshots/redis-monitor.png) + +在 Redis 查询编辑器中单独执行 `MONITOR`,结果区会持续显示当前节点的命令日志,而不是只返回初始 `OK`。监听范围包含该节点的所有 DB,不受编辑器当前选择的 DB 限制。 + +点击“停止监听”或执行工具栏的停止按钮结束监听;关闭页签也会释放专用连接。界面保留最近 1000 条记录,单条超长记录最多显示 16384 个字符。监听结束后保留已显示的结果。 + +目前支持直连 Redis 节点及其 TLS、隧道配置。Sentinel 和 Cluster 连接请另建目标节点的直连连接;不会自动汇总集群日志。Redis 账号需要具备执行 `MONITOR` 的权限,持续监听会增加服务端开销。 + ## 安全与性能建议 - 生产 Redis 使用只读账号或 ACL,并在 DBX 启用只读/生产保护 diff --git a/docs/content/docs/redis.mdx b/docs/content/docs/redis.mdx index 272c02c641..40e16c6949 100644 --- a/docs/content/docs/redis.mdx +++ b/docs/content/docs/redis.mdx @@ -114,6 +114,16 @@ Key statistics and Slowlog require additional Redis commands. Limit scope and en Flush, bulk delete, and `FLUSH*` operations cannot be undone. Read-only connections reject writes, while production and dangerous-command checks remain active. +## Live MONITOR + +![Live Redis MONITOR logs](/screenshots/redis-monitor.png) + +Run `MONITOR` on its own in the Redis query editor to receive live command logs instead of only the initial `OK`. Monitoring covers all databases on the connected node, regardless of the database selected in the editor. + +Use **Stop listening**, the execution toolbar's stop button, or close the tab to release the dedicated connection. The result retains the latest 1,000 entries, with each entry limited to 16,384 characters. Stopping preserves the displayed results. + +Direct Redis node connections, including their TLS and tunnel settings, are supported. For Sentinel or Cluster, create a direct connection to the node to inspect; logs are not aggregated across nodes. The Redis account needs permission to execute `MONITOR`, which adds server overhead while active. + ## Safety and Performance Guidance - Use a read-only account or ACL for production and enable DBX read-only/production protection diff --git a/docs/public/screenshots/redis-monitor.png b/docs/public/screenshots/redis-monitor.png new file mode 100644 index 0000000000000000000000000000000000000000..ea95b5e7521c096b7d6153026ade9839fc0c2949 GIT binary patch literal 72006 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 O2y`^`ab;>2A^!`R*TN70 literal 0 HcmV?d00001 diff --git a/src-tauri/src/commands/redis_pubsub_server.rs b/src-tauri/src/commands/redis_pubsub_server.rs index 295ee84bd0..2973d8cfe3 100644 --- a/src-tauri/src/commands/redis_pubsub_server.rs +++ b/src-tauri/src/commands/redis_pubsub_server.rs @@ -56,6 +56,8 @@ impl Drop for PubSubServerState { #[serde(rename_all = "camelCase")] struct PubSubWsParams { connection_id: String, + #[serde(default)] + monitor: bool, } pub fn build_pubsub_router(state: Arc) -> Router { @@ -93,7 +95,56 @@ async fn ws_handler( State(state): State>, ) -> impl IntoResponse { let connection_id = params.connection_id; - ws.on_upgrade(move |socket| handle_socket(socket, state, connection_id)) + ws.on_upgrade(move |socket| async move { + if params.monitor { + handle_monitor_socket(socket, state, connection_id).await; + } else { + handle_socket(socket, state, connection_id).await; + } + }) +} + +async fn handle_monitor_socket(mut socket: WebSocket, state: Arc, connection_id: String) { + let monitor = tokio::select! { + result = dbx_core::redis_ops::redis_create_monitor_core(&state, &connection_id) => result, + _ = socket.recv() => return, + }; + let monitor = match monitor { + Ok(monitor) => monitor, + Err(error) => { + let _ = socket.send(Message::Text(serde_json::json!({ "error": error }).to_string().into())).await; + return; + } + }; + if socket.send(Message::Text(serde_json::json!({ "ready": true }).to_string().into())).await.is_err() { + return; + } + let mut messages = monitor.into_on_message::(); + let (mut sender, mut receiver) = socket.split(); + loop { + tokio::select! { + incoming = receiver.next() => { + match incoming { + Some(Ok(Message::Ping(payload))) => { + if sender.send(Message::Pong(payload)).await.is_err() { break; } + } + Some(Ok(Message::Pong(_))) => {} + _ => break, + } + } + message = messages.next() => { + let Some(message) = message else { + let _ = sender.send(Message::Text(serde_json::json!({ "error": "Redis MONITOR connection closed" }).to_string().into())).await; + break; + }; + let event = Message::Text(serde_json::json!({ "message": message }).to_string().into()); + match tokio::time::timeout(std::time::Duration::from_secs(5), sender.send(event)).await { + Ok(Ok(())) => {} + _ => break, + } + } + } + } } async fn handle_socket(socket: WebSocket, state: Arc, connection_id: String) {