diff --git a/crates/core/src/parser/json.rs b/crates/core/src/parser/json.rs index d70077c..d2b234f 100644 --- a/crates/core/src/parser/json.rs +++ b/crates/core/src/parser/json.rs @@ -16,7 +16,14 @@ // 类似于 Python 的 dict、JavaScript 的 Object/Map、Java 的 HashMap。 // 它存储键值对(key-value pairs),可以通过键快速查找对应的值。 // 这里用 HashMap 来存储 JSON 中除 timestamp/level 之外的其他字段。 -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; + +// serde::de 中的 Visitor / MapAccess 允许我们自定义 JSON 对象的反序列化过程。 +// 默认的 serde_json::from_str::>() 遇到重复键时会采用"后者覆盖前者"(last-wins), +// 前面的值被静默丢弃。这里我们通过自定义 Visitor 在反序列化过程中逐个观察 key-value 对, +// 在保持 last-wins 行为的同时,将重复 key 的所有值记录到 DuplicateKey 中。 +use serde::de::{MapAccess, Visitor}; +use serde::Deserializer; // serde_json::Value — 来自 serde_json 库(Rust 中最流行的 JSON 处理库)。 // Value 是一个枚举类型,可以表示任意 JSON 值: @@ -32,7 +39,7 @@ use serde_json::Value; // ─── 引入项目内部类型 ────────────────────────────────────────────────────── // crate 表示"当前项目(crate)"。 // types 模块中定义了 LogEntry(一条日志记录)和 LogLevel(日志级别,如 INFO/ERROR)。 -use crate::types::{LogEntry, LogLevel}; +use crate::types::{DuplicateKey, LogEntry, LogLevel}; // ─── strip_bom 辅助函数 ────────────────────────────────────────────────── // 剥离行首的 UTF-8 BOM(Byte Order Mark, U+FEFF)。 @@ -79,32 +86,83 @@ pub fn detect_json_log(line: &str) -> bool { matches!(serde_json::from_str::(strip_bom(line)), Ok(Value::Object(_))) } -// ─── take_string_field 辅助函数 ────────────────────────────────────────────── -// 从 HashMap 中安全提取字符串字段。 +// ─── DuplicateKeyVisitor ────────────────────────────────────────────────── +// 自定义 serde Visitor,在反序列化 JSON 对象时检测重复 key。 // -// 遍历候选键名列表,找到第一个值为字符串类型的字段,移除并返回其值。 -// 如果值不是字符串(如数字、布尔值等),保留该字段在 HashMap 中,继续尝试下一个候选键。 -// 这样可以避免非字符串类型的字段被静默丢弃(数据丢失 bug)。 +// 工作原理: +// serde 的 MapAccess trait 允许我们逐个遍历 JSON 对象的 key-value 对。 +// 每读到一个 (key, value),我们: +// 1. 检查这个 key 是否已经见过(通过 HashSet) +// 2. 如果是重复 key,记录到 Vec 中(包含所有出现过的值) +// 3. 将 key-value 插入 Map(last-wins,与 serde_json 默认行为一致) // -// 参数: -// - fields: &mut HashMap — JSON 字段的 HashMap(可变引用)。 -// - keys: &[&str] — 候选键名列表(按优先级排列)。 -// 返回: Option — 找到字符串值返回 Some(String),否则返回 None。 -fn take_string_field(fields: &mut HashMap, keys: &[&str]) -> Option { +// 这样既保持了兼容性(last-wins),又不丢失信息(所有值都记录在 DuplicateKey 中)。 +struct DuplicateKeyVisitor; + +impl<'de> Visitor<'de> for DuplicateKeyVisitor { + // 返回类型:(serde_json::Map, 重复 key 列表) + type Value = (serde_json::Map, Vec); + + fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + f.write_str("a JSON object") + } + + fn visit_map(self, mut access: A) -> Result + where + A: MapAccess<'de>, + { + let mut map = serde_json::Map::new(); + let mut seen = HashSet::new(); + let mut duplicates: Vec = Vec::new(); + + while let Some((key, value)) = access.next_entry::()? { + if !seen.insert(key.clone()) { + // 重复 key:将之前 map 中的值和当前值都记录下来 + if let Some(existing) = duplicates.iter_mut().find(|d| d.key == key) { + // 同一个 key 第三次及以上出现:追加当前值 + existing.values.push(value.clone()); + } else { + // 同一个 key 第二次出现:记录第一次的值 + 当前值 + let prev_value = map.get(&key).cloned().unwrap_or(Value::Null); + duplicates.push(DuplicateKey { + key: key.clone(), + values: vec![prev_value, value.clone()], + }); + } + } + // last-wins:后出现的值覆盖前面的值,与 serde_json 默认行为一致 + map.insert(key, value); + } + + Ok((map, duplicates)) + } +} + +/// 使用自定义 Visitor 解析 JSON 对象,同时检测重复 key。 +/// +/// 返回 (serde_json::Map, Vec): +/// - Map 中存储所有 key-value(重复 key 取 last-wins) +/// - Vec 中记录所有重复 key 及其全部值 +fn parse_json_object_with_duplicates( + json: &str, +) -> Option<(serde_json::Map, Vec)> { + let mut deserializer = serde_json::Deserializer::from_str(json); + Some(deserializer.deserialize_map(DuplicateKeyVisitor).ok()?) +} + +// ─── take_string_field_from_map 辅助函数 ────────────────────────────────── +// 从 serde_json::Map 中安全提取字符串字段。 +// 功能与原 take_string_field 相同,但操作 serde_json::Map 而非 HashMap。 +fn take_string_field_from_map( + obj: &mut serde_json::Map, + keys: &[&str], +) -> Option { for key in keys { - // 先检查值是否为字符串类型(peek,不移除)。 - // is_some_and(Value::is_string) 等价于: - // match fields.get(*key) { - // Some(Value::String(_)) => true, - // _ => false, - // } - if fields.get(*key).is_some_and(Value::is_string) { - // 确认是字符串后才移除,取出 owned String(无需 clone)。 - // unreachable! 在这里永远不会触发,因为我们刚刚确认了值的类型。 - let Some(Value::String(value)) = fields.remove(*key) else { + if obj.get(*key).is_some_and(Value::is_string) { + let Some(Value::String(v)) = obj.remove(*key) else { unreachable!("value was checked as string"); }; - return Some(value); + return Some(v); } } None @@ -117,65 +175,34 @@ fn take_string_field(fields: &mut HashMap, keys: &[&str]) -> Opti // 返回: Option — 解析成功返回 Some(LogEntry),失败或不合法返回 None。 // Option 是 Rust 的可选类型:Some(值) 表示有值,None 表示没有值。 pub fn parse_line(line: &str) -> Option { - // ─── 跳过空行 ────────────────────────────────────────────────────────── let line = strip_bom(line); - // line.trim() 去除首尾空白字符(空格、制表符、换行符等)。 - // .is_empty() 检查是否为空字符串。 - // 如果去除空白后是空的,说明是空行,不需要解析,直接返回 None。 if line.trim().is_empty() { return None; } - // ─── 解析 JSON 为 HashMap ────────────────────────────────────────────── - // serde_json::from_str(line) 尝试将字符串解析为 JSON。 - // 由于我们声明了 HashMap 类型,Rust 会自动将 JSON 对象 - // 转换为 HashMap,其中每个键是 String,每个值是 serde_json::Value。 - // - // .ok() 将 Result 转换为 Option: - // Ok(值) → Some(值) - // Err(_) → None - // - // 末尾的 ? 是"问号操作符"(try operator),在这里的作用是: - // 如果 .ok() 返回 None(即 JSON 解析失败),则整个函数直接返回 None。 - // 如果返回 Some(hashmap),则将 hashmap 取出并绑定到 fields 变量。 - // - // let mut 表示这是一个"可变变量"(mutable variable), - // 后续代码会修改这个 HashMap(从中删除已识别的字段)。 - let mut fields: HashMap = serde_json::from_str(line).ok()?; + // ─── 使用自定义 Visitor 解析 JSON ────────────────────────────────── + // 通过 DuplicateKeyVisitor 反序列化,在保持 last-wins 的同时检测重复 key。 + // 返回的 (serde_json::Map, Vec) 中: + // - Map 包含所有 key-value(重复 key 取最后一个值) + // - Vec 记录了所有重复 key 及其出现过的全部值 + let (mut obj, duplicate_keys) = parse_json_object_with_duplicates(line)?; - // ─── 保存原始行内容 ────────────────────────────────────────────────── - // line.to_string() 将 &str(字符串切片引用)转换为 String(拥有所有权的字符串)。 - // 保存原始行是为了在 UI 中显示未经修改的原始日志内容。 let raw_line = line.to_string(); - - // ─── 提取时间戳字段 ────────────────────────────────────────────────── - // 使用 take_string_field 辅助函数安全提取字符串类型的时间戳。 - // 只有值为字符串时才会从 fields 中移除;非字符串值(如数字时间戳)保留在 fields 中。 - let timestamp = take_string_field(&mut fields, &["timestamp", "time", "ts", "@timestamp"]); - - // ─── 提取日志级别字段 ────────────────────────────────────────────── - // 与时间戳提取类似,但多了一步:将字符串解析为 LogLevel 枚举。 - let level = take_string_field(&mut fields, &["level", "lvl", "severity"]) + let timestamp = take_string_field_from_map(&mut obj, &["timestamp", "time", "ts", "@timestamp"]); + let level = take_string_field_from_map(&mut obj, &["level", "lvl", "severity"]) .map(|s| s.parse::().unwrap_or_else(|e| match e {})); - // ─── 构建 LogEntry 并返回 ────────────────────────────────────────── - // 此时 fields HashMap 中还剩下未被提取的字段(如 message、自定义字段等)。 - // timestamp 和 level 已经从 fields 中移除了(通过 remove)。 - // - // Some(LogEntry { ... }) — 使用结构体字面量创建 LogEntry 实例, - // 并用 Some() 包裹表示"有值"。 + // serde_json::Map → HashMap:剩余字段转为 HashMap 存入 fields + let fields: HashMap = obj.into_iter().collect(); + Some(LogEntry { - // line_number 设为 0,由调用者(如 parse_line_with_number)设置正确的值。 line_number: 0, - // 原始行内容。 raw_line, - // 时间戳(可能为 None,如果 JSON 中没有时间戳字段)。 timestamp, - // 日志级别(可能为 None,如果 JSON 中没有级别字段)。 level, - // 剩余的 JSON 字段(已移除 timestamp 和 level)。 fields, + duplicate_keys, }) } @@ -463,4 +490,77 @@ mod tests { Some(&Value::String("\u{FEFF}hello".into())) ); } + + // ─── 重复 key 检测测试 ────────────────────────────────────────────── + + #[test] + fn test_no_duplicate_keys_normal_json() { + let line = r#"{"level":"INFO","message":"hello"}"#; + let entry = parse_line(line).unwrap(); + assert!(entry.duplicate_keys.is_empty()); + } + + #[test] + fn test_duplicate_message_key_detected() { + let line = r#"{"level":"INFO","message":"first","message":"second"}"#; + let entry = parse_line(line).unwrap(); + // last-wins: fields 中保留第二个值 + assert_eq!( + entry.fields.get("message"), + Some(&Value::String("second".into())) + ); + // 重复 key 记录中包含所有值 + assert_eq!(entry.duplicate_keys.len(), 1); + assert_eq!(entry.duplicate_keys[0].key, "message"); + assert_eq!(entry.duplicate_keys[0].values.len(), 2); + assert_eq!(entry.duplicate_keys[0].values[0], Value::String("first".into())); + assert_eq!(entry.duplicate_keys[0].values[1], Value::String("second".into())); + } + + #[test] + fn test_duplicate_key_last_wins() { + let line = r#"{"msg":"a","msg":"b","msg":"c"}"#; + let entry = parse_line(line).unwrap(); + // last-wins: 最终值是 "c" + assert_eq!(entry.fields.get("msg"), Some(&Value::String("c".into()))); + // 三个值都被记录 + assert_eq!(entry.duplicate_keys.len(), 1); + assert_eq!(entry.duplicate_keys[0].values.len(), 3); + assert_eq!(entry.duplicate_keys[0].values[0], Value::String("a".into())); + assert_eq!(entry.duplicate_keys[0].values[1], Value::String("b".into())); + assert_eq!(entry.duplicate_keys[0].values[2], Value::String("c".into())); + } + + #[test] + fn test_multiple_different_duplicate_keys() { + let line = r#"{"a":"1","b":"2","a":"3","b":"4"}"#; + let entry = parse_line(line).unwrap(); + assert_eq!(entry.duplicate_keys.len(), 2); + let dup_a = entry.duplicate_keys.iter().find(|d| d.key == "a").unwrap(); + let dup_b = entry.duplicate_keys.iter().find(|d| d.key == "b").unwrap(); + assert_eq!(dup_a.values.len(), 2); + assert_eq!(dup_b.values.len(), 2); + } + + #[test] + fn test_duplicate_level_key_detected() { + let line = r#"{"level":"INFO","level":"ERROR","message":"hello"}"#; + let entry = parse_line(line).unwrap(); + // last-wins: level 被提取为 ERROR + assert_eq!(entry.level, Some(LogLevel::Error)); + // 重复 key 被记录 + assert_eq!(entry.duplicate_keys.len(), 1); + assert_eq!(entry.duplicate_keys[0].key, "level"); + assert_eq!(entry.duplicate_keys[0].values.len(), 2); + } + + #[test] + fn test_duplicate_timestamp_key_detected() { + let line = r#"{"timestamp":"2024-01-01","timestamp":"2024-06-01"}"#; + let entry = parse_line(line).unwrap(); + // last-wins: timestamp 提取为后者 + assert_eq!(entry.timestamp, Some("2024-06-01".to_string())); + assert_eq!(entry.duplicate_keys.len(), 1); + assert_eq!(entry.duplicate_keys[0].key, "timestamp"); + } } diff --git a/crates/core/src/types.rs b/crates/core/src/types.rs index 45112dd..5c6276f 100644 --- a/crates/core/src/types.rs +++ b/crates/core/src/types.rs @@ -123,6 +123,18 @@ impl fmt::Display for LogLevel { } // ─── LogEntry 结构体 ──────────────────────────────────────────────────────── +/// 记录 JSON 日志中出现的重复 key 信息 +/// +/// 当 JSON 对象中同一个 key 出现多次时,serde_json 默认 last-wins(后值覆盖前值), +/// 前面的值会静默丢失。此结构记录所有重复出现的 key 及其全部值,供 UI 展示警告。 +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct DuplicateKey { + /// 重复的 key 名称 + pub key: String, + /// 该 key 出现的所有值(按出现顺序排列,最后一个值是 fields 中的最终值) + pub values: Vec, +} + /// 一行解析后的日志 /// /// 表示日志文件中经过解析器处理后的一行内容。 @@ -148,6 +160,9 @@ pub struct LogEntry { /// HashMap 是一个字典,键是字段名,值是 JSON 值。 /// 例如 {"message": "hello", "request_id": "abc123"}。 pub fields: HashMap, + + /// JSON 中重复出现的 key 记录(正常日志为空 Vec) + pub duplicate_keys: Vec, } // ─── SearchResult 结构体 ──────────────────────────────────────────────────── @@ -351,6 +366,7 @@ mod tests { timestamp: Some("2024-01-01T00:00:00".to_string()), level: Some(LogLevel::Info), fields, + duplicate_keys: vec![], }; // 逐字段验证。