@@ -16,7 +16,14 @@
// 类似于 Python 的 dict、JavaScript 的 Object/Map、Java 的 HashMap。
// 它存储键值对( key-value pairs) , 可以通过键快速查找对应的值。
// 这里用 HashMap<String, Value> 来存储 JSON 中除 timestamp/level 之外的其他字段。
use std ::collections ::HashMap ;
use std ::collections ::{ HashMap , HashSet } ;
// serde::de 中的 Visitor / MapAccess 允许我们自定义 JSON 对象的反序列化过程。
// 默认的 serde_json::from_str::<HashMap<_, _>>() 遇到重复键时会采用"后者覆盖前者"( 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 ::< Value > ( 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<DuplicateKey> 中(包含所有出现过的值)
// 3. 将 key-value 插入 Map( last-wins, 与 serde_json 默认行为一致)
//
// 参数:
// - fields: &mut HashMap<String, Value> — JSON 字段的 HashMap( 可变引用) 。
// - keys: &[&str] — 候选键名列表(按优先级排列)。
// 返回: Option<String> — 找到字符串值返回 Some(String),否则返回 None。
fn take_string_field ( fields : & mut HashMap < String , Value > , keys : & [ & str ] ) -> Option < String > {
// 这样既保持了兼容性( last-wins) , 又不丢失信息( 所有值都记录在 DuplicateKey 中)。
struct DuplicateKeyVisitor ;
impl < ' de > Visitor < ' de > for DuplicateKeyVisitor {
// 返回类型:(serde_json::Map, 重复 key 列表)
type Value = ( serde_json ::Map < String , Value > , Vec < DuplicateKey > ) ;
fn expecting ( & self , f : & mut std ::fmt ::Formatter ) -> std ::fmt ::Result {
f . write_str ( " a JSON object " )
}
fn visit_map < A > ( self , mut access : A ) -> Result < Self ::Value , A ::Error >
where
A : MapAccess < ' de > ,
{
let mut map = serde_json ::Map ::new ( ) ;
let mut seen = HashSet ::new ( ) ;
let mut duplicates : Vec < DuplicateKey > = Vec ::new ( ) ;
while let Some ( ( key , value ) ) = access . next_entry ::< String , Value > ( ) ? {
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<DuplicateKey>):
/// - Map 中存储所有 key-value( 重复 key 取 last-wins)
/// - Vec 中记录所有重复 key 及其全部值
fn parse_json_object_with_duplicates (
json : & str ,
) -> Option < ( serde_json ::Map < String , Value > , Vec < DuplicateKey > ) > {
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 < String , Value > ,
keys : & [ & str ] ,
) -> Option < String > {
for key in keys {
// 先检查值是否为字符串类型( peek, 不移除) 。
// is_some_and( Value::is_s tring) 等价于:
// 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 ::S tring( 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<String, Value>, keys: &[&str]) -> Opti
// 返回: Option<LogEntry> — 解析成功返回 Some(LogEntry),失败或不合法返回 None。
// Option 是 Rust 的可选类型: Some(值) 表示有值, None 表示没有值。
pub fn parse_line ( line : & str ) -> Option < LogEntry > {
// ─── 跳过空行 ──────────────────────────────────────────────────────────
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<String, Value> 类型, 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 < String , Value > = serde_json ::from_str ( line ) . ok ( ) ? ;
// ─── 使用自定义 Visitor 解析 JSON ──────────────────────────────────
// 通过 DuplicateKeyVisitor 反序列化,在保持 last-wins 的同时检测重复 key 。
// 返回的 (serde_json::Map, Vec<DuplicateKey>) 中:
// - 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 ::< LogLevel > ( ) . 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< String , Value > = 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 ,
} )
}
@@ -319,6 +346,13 @@ mod tests {
assert_eq! ( parse_line ( warn_line ) . unwrap ( ) . level , Some ( LogLevel ::Warn ) ) ;
}
#[ test ]
// Regression: level field with surrounding whitespace should still be recognized.
fn test_level_whitespace_in_json ( ) {
let line = r # "{"level":" WARN ","message":"test"}"# ;
assert_eq! ( parse_line ( line ) . unwrap ( ) . level , Some ( LogLevel ::Warn ) ) ;
}
#[ test ]
// 测试: 所有候选时间戳键名( timestamp, time, ts, @timestamp) 都能被识别。
fn test_timestamp_key_names ( ) {
@@ -456,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 " ) ;
}
}