fix(redis): unify INFO category command output in cluster mode

* fix(redis):format INFO output as plain text in cluster mode

* fix(redis): unify INFO category command output in cluster mode

In Redis cluster mode, the INFO command (with category parameters) returns aggregated results from multiple primaries. This fix formats the output as human-readable plain text, consistent with standalone mode, to improve readability.

Closes #1326
This commit is contained in:
dalew
2026-06-17 11:20:07 +08:00
committed by GitHub
parent 66e6ca1211
commit 8486d27cb2
2 changed files with 27 additions and 15 deletions
+11
View File
@@ -20,6 +20,17 @@ export function redisCommandResultToQueryResult(value: unknown, elapsedMs: numbe
execution_time_ms: Math.max(0, Math.round(elapsedMs)),
};
}
// INFO commands in cluster mode → [[node_addr, info_text], ...] pairs.
// Render as a two-column table with the addr as the index and info text as the value.
if (Array.isArray(value) && value.length > 0 && value.every((item) => Array.isArray(item) && item.length === 2 && typeof item[0] === "string" && typeof item[1] === "string")) {
const rows: (string | number | boolean | null)[][] = value.map((item: [string, string]) => [item[0], item[1]]);
return {
columns: ["(index)", "value"],
rows,
affected_rows: value.length,
execution_time_ms: Math.max(0, Math.round(elapsedMs)),
};
}
if (Array.isArray(value)) {
const rows: (string | number | boolean | null)[][] = value.map((item, i) => [i + 1, formatRedisCommandResult(item)]);
return {
+16 -15
View File
@@ -1014,27 +1014,28 @@ where
// Special handling for INFO command in cluster mode.
// redis-rs ClusterConnection routes INFO to all primaries and
// aggregates the results as a Map(node_addr → full_info_text).
// We detect this pattern and format it as human-readable plain text
// instead of converting to a JSON array of {key, value} objects.
// We detect this pattern and return it as an array of
// [node_addr, info_text] pairs, which the frontend renders
// as a two-column (index → node_addr, value → info_text) table.
if command == "INFO" {
if let RedisRawValue::Map(entries) = &raw {
// Cluster-aggregated INFO has multi-line values starting with "# Server".
// Cluster-aggregated INFO has multi-line values starting with "# "
// (e.g. "# Server", "# Memory", "# Clients").
// This distinguishes it from a RESP3 standalone INFO map where values
// are single field values (e.g. "redis_version", "os").
// are single field values (e.g. "redis_version", "os") or nested maps.
let is_cluster_aggregation =
entries.iter().any(|(_, v)| redis_raw_value_as_str(v).is_some_and(|s| s.starts_with("# Server")));
entries.iter().any(|(_, v)| redis_raw_value_as_str(v).is_some_and(|s| s.starts_with("# ")));
if is_cluster_aggregation {
let mut parts: Vec<String> = Vec::with_capacity(entries.len());
for (key, value) in entries {
let addr = redis_raw_value_as_str(key);
let info = redis_raw_value_as_str(value);
if let (Some(addr), Some(info)) = (addr, info) {
parts.push(format!("{addr}\n{info}"));
}
}
let text = parts.join("\n\n");
return Ok(RedisCommandResult { command, safety, value: serde_json::Value::String(text) });
let pairs: Vec<serde_json::Value> = entries
.iter()
.filter_map(|(key, value)| {
let addr = redis_raw_value_as_str(key)?;
let info = redis_raw_value_as_str(value)?;
Some(serde_json::json!([addr, info]))
})
.collect();
return Ok(RedisCommandResult { command, safety, value: serde_json::Value::Array(pairs) });
}
}
}