use forge::signal::compactor;
use once_cell::sync::Lazy;
use regex::Regex;
static INFO_SECTION_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"(?m)^# \w+\s*$").unwrap());
const INFO_KEEP: &[&str] = &[
"redis_version",
"uptime_in_seconds",
"connected_clients",
"used_memory_human",
"used_memory_peak_human",
"mem_fragmentation_ratio",
"rdb_last_bgsave_status",
"aof_enabled",
"role",
"connected_slaves",
"master_replid",
"db0:",
"db1:",
"db2:",
"db3:",
"db4:",
"db5:",
"db6:",
"db7:",
"db8:",
"db9:",
"total_commands_processed",
"instantaneous_ops_per_sec",
"total_net_input_bytes",
"keyspace_hits",
"keyspace_misses",
];
pub fn compress_keys(raw: &str) -> String {
let cleaned = compactor::normalise(raw);
let lines: Vec<&str> = cleaned.lines().filter(|l| !l.trim().is_empty()).collect();
if lines.len() > 50 {
return format!(
"{}\n… [{} more keys — use SCAN with MATCH/COUNT or KEYS pattern:*] …",
lines[..50].join("\n"),
lines.len() - 50
);
}
lines.join("\n")
}
pub fn compress_info(raw: &str) -> String {
let cleaned = compactor::normalise(raw);
let mut out: Vec<&str> = Vec::new();
let mut current_section = "";
for line in cleaned.lines() {
let t = line.trim();
if t.is_empty() {
continue;
}
if INFO_SECTION_RE.is_match(t) {
current_section = t;
out.push(line);
continue;
}
let _ = current_section;
if INFO_KEEP.iter().any(|k| t.starts_with(k)) {
out.push(line);
}
}
if out.is_empty() {
return compress_generic(raw);
}
out.join("\n")
}
pub fn compress_debug(raw: &str) -> String {
let cleaned = compactor::normalise(raw);
let useful: Vec<&str> = cleaned
.lines()
.filter(|l| {
let t = l.trim();
!t.is_empty()
&& !t.contains("ql-nodes")
&& !t.contains("ziplist")
&& !t.contains("listpack")
&& !t.contains("refcount")
&& (t.contains("encoding")
|| t.contains("serializedlength")
|| t.contains("lru_seconds")
|| t.contains("type:")
|| t.starts_with("Value at:")
|| t.contains("error")
|| t.contains("Error"))
})
.collect();
if useful.is_empty() {
return compress_generic(raw);
}
useful.join("\n")
}
pub fn compress_generic(raw: &str) -> String {
let cleaned = compactor::normalise(raw);
let lines: Vec<&str> = cleaned.lines().filter(|l| !l.trim().is_empty()).collect();
if lines.len() > 40 {
return format!(
"{}\n… [{} more lines] …",
lines[..40].join("\n"),
lines.len() - 40
);
}
lines.join("\n")
}
pub fn compress_redis(raw: &str) -> String {
let t = raw.trim();
if t.contains("redis_version:") || t.contains("# Server") {
return compress_info(raw);
}
let lines: Vec<&str> = t.lines().collect();
let looks_like_keys = lines.len() > 5
&& lines
.iter()
.take(5)
.all(|l| !l.contains(':') || l.trim().starts_with('"'));
if looks_like_keys {
return compress_keys(raw);
}
compress_generic(raw)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn keys_truncates_large_keyspace() {
let keys: Vec<String> = (0..60).map(|i| format!("user:session:{i}")).collect();
let out = compress_keys(&keys.join("\n"));
assert!(out.contains("more keys"), "{out}");
assert!(out.contains("SCAN"), "{out}");
}
#[test]
fn info_keeps_signal_fields() {
let raw = "# Server\nredis_version:7.2.0\nos:Linux 5.15.0\narch_bits:64\n# Clients\nconnected_clients:42\nblocked_clients:0\n# Memory\nused_memory_human:1.23M\nused_memory_peak_human:2.00M\nmem_fragmentation_ratio:1.05\ntotal_system_memory_human:16.00G\n";
let out = compress_info(raw);
assert!(out.contains("redis_version"), "{out}");
assert!(out.contains("connected_clients"), "{out}");
assert!(out.contains("used_memory_human"), "{out}");
assert!(!out.contains("arch_bits"), "{out}");
assert!(!out.contains("total_system_memory_human"), "{out}");
}
#[test]
fn info_keeps_keyspace_db_lines() {
let raw = "# Keyspace\ndb0:keys=1234,expires=56,avg_ttl=78900\n";
let out = compress_info(raw);
assert!(out.contains("db0:"), "{out}");
}
#[test]
fn generic_truncates_large_output() {
let lines: Vec<String> = (0..50).map(|i| format!("line {i}: value")).collect();
let out = compress_generic(&lines.join("\n"));
assert!(out.contains("more lines"), "{out}");
}
}