diff --git a/crates/core/src/parser/json.rs b/crates/core/src/parser/json.rs index bd78578..863fd33 100644 --- a/crates/core/src/parser/json.rs +++ b/crates/core/src/parser/json.rs @@ -1,73 +1,248 @@ +// ─── json.rs ───────────────────────────────────────────────────────────────── +// 这个文件负责解析 JSON 格式的日志行。 +// +// JSON 日志的典型格式如下(一行一条日志,也叫 "JSON Lines" 或 "NDJSON"): +// {"level":"INFO","message":"hello","timestamp":"2024-01-01T00:00:00Z"} +// +// 本文件的函数会: +// 1. 判断一行是否是 JSON 对象(detect_json_log) +// 2. 将 JSON 行解析为 LogEntry 结构体(parse_line) +// ────────────────────────────────────────────────────────────────────────────── + +// ─── 引入外部依赖 ────────────────────────────────────────────────────────── +// `use` 语句引入其他模块中定义的类型,类似于 Python 的 import。 + +// std::collections::HashMap — Rust 标准库中的哈希表(字典)类型。 +// 类似于 Python 的 dict、JavaScript 的 Object/Map、Java 的 HashMap。 +// 它存储键值对(key-value pairs),可以通过键快速查找对应的值。 +// 这里用 HashMap 来存储 JSON 中除 timestamp/level 之外的其他字段。 use std::collections::HashMap; +// serde_json::Value — 来自 serde_json 库(Rust 中最流行的 JSON 处理库)。 +// Value 是一个枚举类型,可以表示任意 JSON 值: +// - Value::Null → JSON 的 null +// - Value::Bool(b) → JSON 的 true / false +// - Value::Number(n) → JSON 的数字(如 42, 3.14) +// - Value::String(s) → JSON 的字符串(如 "hello") +// - Value::Array(a) → JSON 的数组(如 [1, 2, 3]) +// - Value::Object(o) → JSON 的对象(如 {"key": "value"}) +// 使用 Value 类型是因为日志中的字段值类型不固定,可能是字符串、数字等。 use serde_json::Value; +// ─── 引入项目内部类型 ────────────────────────────────────────────────────── +// crate 表示"当前项目(crate)"。 +// types 模块中定义了 LogEntry(一条日志记录)和 LogLevel(日志级别,如 INFO/ERROR)。 use crate::types::{LogEntry, LogLevel}; +// ─── detect_json_log 函数 ────────────────────────────────────────────────── +// 检测一行文本是否是一个 JSON 对象。 +// +// 参数: line: &str — 一行文本的不可变引用(只读字符串切片)。 +// 返回: bool — true 表示是 JSON 对象,false 表示不是。 +// +// 为什么要区分"JSON 对象"和"其他 JSON"? +// 因为日志行必须是 JSON 对象(花括号 {} 包裹的键值对)才有意义。 +// JSON 数组 [1,2,3]、JSON 字符串 "hello"、JSON 数字 42 等都不是有效的日志行。 pub fn detect_json_log(line: &str) -> bool { + // matches! 是 Rust 的一个宏(macro),用于简化 match 表达式。 + // 完整写法等价于: + // match serde_json::from_str::(line) { + // Ok(Value::Object(_)) => true, + // _ => false, + // } + // + // 拆解这段代码: + // + // 1. serde_json::from_str::(line) + // 尝试将字符串 line 解析为 JSON 值(Value 类型)。 + // :: 是"泛型参数"(turbofish 语法),告诉 from_str 我们想要 Value 类型。 + // 返回 Result:成功返回 Ok(Value),失败返回 Err(Error)。 + // + // 2. Ok(Value::Object(_)) + // 模式匹配:如果解析成功(Ok),且结果是 JSON 对象(Value::Object), + // 则匹配成功。_ 是通配符,表示"不关心对象里面的具体内容"。 + // + // 如果匹配到 Ok(Value::Object(_)) 返回 true,否则返回 false。 matches!(serde_json::from_str::(line), Ok(Value::Object(_))) } +// ─── parse_line 函数 ────────────────────────────────────────────────────── +// 将一行 JSON 文本解析为 LogEntry(日志条目)结构体。 +// +// 参数: line: &str — 一行日志文本。 +// 返回: Option — 解析成功返回 Some(LogEntry),失败或不合法返回 None。 +// Option 是 Rust 的可选类型:Some(值) 表示有值,None 表示没有值。 pub fn parse_line(line: &str) -> Option { + // ─── 跳过空行 ────────────────────────────────────────────────────────── + // 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()?; + // ─── 保存原始行内容 ────────────────────────────────────────────────── + // line.to_string() 将 &str(字符串切片引用)转换为 String(拥有所有权的字符串)。 + // 保存原始行是为了在 UI 中显示未经修改的原始日志内容。 let raw_line = line.to_string(); + // ─── 提取时间戳字段 ────────────────────────────────────────────────── + // 这段代码尝试从 JSON 中提取时间戳,逻辑如下: + // + // 1. ["timestamp", "time", "ts", "@timestamp"] — 候选键名数组。 + // 不同日志系统使用不同的时间戳字段名,这里列出常见的几种。 + // + // 2. .iter() — 创建数组的迭代器,可以逐个遍历元素。 + // + // 3. .find_map(|key| fields.remove(*key)) — 对每个候选键名: + // - fields.remove(*key): 尝试从 HashMap 中移除该键并返回对应的值。 + // 如果键不存在,remove 返回 None。 + // *key 是解引用(deref),将 &str(引用)转换为 str,因为 remove 接受 &str 类型。 + // - find_map: 遍历所有候选键,返回第一个 Some(值) 的结果。 + // 即找到第一个存在的键就停止。 + // + // 4. .and_then(|v| v.as_str().map(String::from)) — 如果找到了时间戳值: + // - v.as_str(): 尝试将 serde_json::Value 转换为 &str(字符串切片)。 + // 如果 Value 不是字符串类型(比如是数字),返回 None。 + // - .map(String::from): 如果是字符串,将其转换为 String(拥有所有权的字符串)。 + // - and_then: 类似于 map,但用于"扁平化"嵌套的 Option。 + // 如果 as_str() 返回 None,整个链返回 None。 let timestamp = ["timestamp", "time", "ts", "@timestamp"] .iter() .find_map(|key| fields.remove(*key)) .and_then(|v| v.as_str().map(String::from)); + // ─── 提取日志级别字段 ────────────────────────────────────────────── + // 与时间戳提取类似,但多了一步:将字符串解析为 LogLevel 枚举。 let level = ["level", "lvl", "severity"] .iter() .find_map(|key| fields.remove(*key)) .and_then(|v| v.as_str().map(String::from)) + // .map(|s| s.parse::(...)) — 尝试将字符串解析为 LogLevel 枚举。 + // parse:: 中的 :: 是泛型参数(turbofish 语法), + // 指定我们要将字符串解析为 LogLevel 类型。 + // + // .unwrap_or_else(|e| match e {}) — 错误处理: + // - 如果解析成功,直接返回 LogLevel 值。 + // - 如果解析失败(字符串不匹配任何已知的日志级别),执行闭包。 + // - |e| match e {}: 这个闭包接收解析错误 e,用 match e {} 进行"穷尽匹配"。 + // 由于 LogLevel 的 parse 错误类型是一个空枚举(没有任何变体), + // match e {} 意味着"这个分支永远不会执行"(unreachable)。 + // 但实际上,如果 parse 失败,unwrap_or_else 不会执行这个闭包—— + // 等等,这里有个细微之处: + // unwrap_or_else 只在 Err 时执行闭包,但 match e {} 对空枚举是合法的 + // (因为空枚举没有任何可能的值,所以 match 是穷尽的)。 + // 不过这里的实际效果是:如果 parse 失败,整个 .map() 返回 None + // (因为 unwrap_or_else 返回的类型是 LogLevel,而空 match 不会有返回值)。 + // + // 实际上更准确的解释:parse() 的错误类型是 Infallible(不可失败的), + // 即解析总是成功。所以 unwrap_or_else 永远不会被执行。 + // 但即使如此,unwrap_or_else 的闭包也需要类型正确,match e {} 满足这一点。 .map(|s| s.parse::().unwrap_or_else(|e| match e {})); + // ─── 构建 LogEntry 并返回 ────────────────────────────────────────── + // 此时 fields HashMap 中还剩下未被提取的字段(如 message、自定义字段等)。 + // timestamp 和 level 已经从 fields 中移除了(通过 remove)。 + // + // Some(LogEntry { ... }) — 使用结构体字面量创建 LogEntry 实例, + // 并用 Some() 包裹表示"有值"。 Some(LogEntry { + // line_number 设为 0,由调用者(如 parse_line_with_number)设置正确的值。 line_number: 0, + // 原始行内容。 raw_line, + // 时间戳(可能为 None,如果 JSON 中没有时间戳字段)。 timestamp, + // 日志级别(可能为 None,如果 JSON 中没有级别字段)。 level, + // 剩余的 JSON 字段(已移除 timestamp 和 level)。 fields, }) } +// ─── parse_line_with_number 函数 ────────────────────────────────────────── +// 与 parse_line 相同,但可以指定行号。 +// 这是一个"便捷函数"(convenience function),避免调用者手动修改 line_number。 +// +// 参数: +// - line: &str — 一行日志文本。 +// - line_number: u64 — 行号(无符号 64 位整数)。 +// 返回: Option — 解析成功返回 Some(LogEntry),失败返回 None。 pub fn parse_line_with_number(line: &str, line_number: u64) -> Option { + // 先调用 parse_line 解析行内容,然后通过 .map() 修改行号。 + // .map() 对 Option 进行转换:如果是 Some(entry),执行闭包;如果是 None,保持 None。 + // + // |mut entry| { ... } — 闭包参数 mut entry 表示"可变的 LogEntry"。 + // 需要 mut 是因为我们要修改 entry.line_number 字段。 parse_line(line).map(|mut entry| { entry.line_number = line_number; entry + // 闭包的最后一条表达式就是返回值(Rust 不需要 return 关键字)。 }) } +// ─── 单元测试 ──────────────────────────────────────────────────────────────── +// `#[cfg(test)]` 表示以下代码只在测试时编译。 +// `mod tests` 定义一个名为 tests 的子模块,用于存放测试函数。 #[cfg(test)] mod tests { + // `use super::*;` — 将父模块(json.rs 中的公开函数)全部引入当前作用域。 use super::*; #[test] + // 测试:解析一个完整的 JSON 日志行,验证所有字段正确提取。 fn test_valid_json_log() { + // r#"..."# 是 Rust 的"原始字符串"(raw string)语法。 + // 在原始字符串中,反斜杠 \ 不会被当作转义字符, + // 所以可以直接写 JSON 而不需要双重转义(如 \\n 代替 \n)。 + // # 符号可以重复多次(如 r##"..."##),用于处理字符串中包含 "# 的情况。 let line = r#"{"level":"INFO","message":"hello","timestamp":"2024-01-01T00:00:00Z"}"#; + // 解析这一行,unwrap() 断言解析成功。 let entry = parse_line(line).unwrap(); + // 验证原始行内容被正确保存。 assert_eq!(entry.raw_line, line); + // 验证时间戳正确提取。Some(...) 表示有值。 assert_eq!(entry.timestamp, Some("2024-01-01T00:00:00Z".to_string())); + // 验证日志级别正确解析为 LogLevel::Info。 assert_eq!(entry.level, Some(LogLevel::Info)); + // 验证 message 字段保留在 fields 中(没有被提取走)。 + // entry.fields.get("message") 从 HashMap 中获取 "message" 键对应的值的引用。 + // Some(&Value::String("hello".to_string())) 是期望值。 assert_eq!( entry.fields.get("message"), Some(&Value::String("hello".to_string())) ); + // 验证行号默认为 0。 assert_eq!(entry.line_number, 0); } #[test] + // 测试:JSON 中缺少 timestamp 和 level 字段时,应返回 None。 fn test_json_missing_fields() { let line = r#"{"message":"just a message"}"#; let entry = parse_line(line).unwrap(); + // 没有 timestamp 字段,应该为 None。 assert!(entry.timestamp.is_none()); + // 没有 level 字段,应该为 None。 assert!(entry.level.is_none()); + // message 字段应该保留在 fields 中。 assert_eq!( entry.fields.get("message"), Some(&Value::String("just a message".to_string())) @@ -75,79 +250,106 @@ mod tests { } #[test] + // 测试:纯文本(非 JSON)应该解析失败,返回 None。 fn test_plain_text() { + // assert! 宏断言表达式为 true。parse_line 返回 None,.is_none() 为 true。 assert!(parse_line("hello world").is_none()); } #[test] + // 测试:空行应该返回 None。 fn test_empty_line() { assert!(parse_line("").is_none()); } #[test] + // 测试:只有空白字符的行应该返回 None。 fn test_whitespace_line() { assert!(parse_line(" ").is_none()); } #[test] + // 测试:JSON 数组不是日志行(detect_json_log 应返回 false)。 fn test_json_array() { + // assert!(!expr) 断言表达式为 false。 + // detect_json_log("[1,2,3]") 返回 false(数组不是对象),取反后为 true。 assert!(!detect_json_log("[1,2,3]")); } #[test] + // 测试:JSON 字符串不是日志行。 fn test_json_string() { assert!(!detect_json_log("\"hello\"")); } #[test] + // 测试:JSON 数字不是日志行。 fn test_json_number() { assert!(!detect_json_log("42")); } #[test] + // 测试:JSON null 不是日志行。 fn test_json_null() { assert!(!detect_json_log("null")); } #[test] + // 测试:JSON 布尔值不是日志行。 fn test_json_bool() { assert!(!detect_json_log("true")); } #[test] + // 测试:不同大小写的日志级别都应该正确解析。 + // LogLevel 实现了不区分大小写的解析("info"、"INFO"、"Info" 都能识别)。 fn test_level_variants() { + // 小写 "info" let info_line = r#"{"level":"info"}"#; assert_eq!(parse_line(info_line).unwrap().level, Some(LogLevel::Info)); + // 大写 "ERROR" let error_line = r#"{"level":"ERROR"}"#; assert_eq!(parse_line(error_line).unwrap().level, Some(LogLevel::Error)); + // 小写 "warn" let warn_line = r#"{"level":"warn"}"#; assert_eq!(parse_line(warn_line).unwrap().level, Some(LogLevel::Warn)); } #[test] + // 测试:所有候选时间戳键名(timestamp, time, ts, @timestamp)都能被识别。 fn test_timestamp_key_names() { + // &[] 创建一个数组的引用,然后遍历其中的每个字符串。 for key in &["timestamp", "time", "ts", "@timestamp"] { + // format! 宏用于格式化字符串,类似 Python 的 f-string。 + // r#"..."# 中的 {key} 会被替换为变量 key 的值。 let line = format!(r#"{{"{key}":"2024-01-01T00:00:00Z"}}"#); + // 注意:format! 中的花括号需要双写 {{ }} 来表示字面量的花括号。 + // 因为单花括号 { } 是格式化占位符。 let entry = parse_line(&line).unwrap(); assert_eq!( entry.timestamp, Some("2024-01-01T00:00:00Z".to_string()), + // 第三个参数是断言失败时的自定义错误信息。 "failed for key: {key}" ); } } #[test] + // 测试:parse_line_with_number 能正确设置行号。 fn test_parse_line_with_number() { let line = r#"{"level":"DEBUG","message":"test"}"#; let entry = parse_line_with_number(line, 42).unwrap(); + // 行号应该被设置为 42。 assert_eq!(entry.line_number, 42); + // 日志级别也应该正确解析。 assert_eq!(entry.level, Some(LogLevel::Debug)); } #[test] + // 测试:所有候选日志级别键名(level, lvl, severity)都能被识别。 fn test_level_key_variants() { for key in &["level", "lvl", "severity"] { let line = format!(r#"{{"{key}":"INFO"}}"#); diff --git a/crates/core/src/parser/mod.rs b/crates/core/src/parser/mod.rs index 22fdbb3..e79a4b9 100644 --- a/crates/core/src/parser/mod.rs +++ b/crates/core/src/parser/mod.rs @@ -1 +1,12 @@ +// ─── parser 模块说明 ───────────────────────────────────────────────────────── +// 这个模块负责"解析"(parse)日志文件中的每一行内容。 +// 日志可能有不同格式(JSON、纯文本等),parser 的职责是: +// 1. 识别一行的格式(是 JSON?还是纯文本?) +// 2. 把这行内容解析成结构化的数据(LogEntry 类型) +// +// 在 Rust 中,mod.rs 是模块的入口文件,通过 `pub mod` 声明子模块。 +// ────────────────────────────────────────────────────────────────────────────── + +// 声明并导出 json 子模块(定义在 json.rs 文件中)。 +// 该模块负责解析 JSON 格式的日志行。 pub mod json;