docs(core): add Chinese comments to parser module

Add detailed Chinese comments explaining Rust syntax, serde_json usage, and JSON log parsing logic.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
dailz
2026-04-11 16:20:44 +08:00
parent e3f11d2165
commit c04cc72b55
2 changed files with 213 additions and 0 deletions

View File

@@ -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<String, Value> 来存储 JSON 中除 timestamp/level 之外的其他字段。
use std::collections::HashMap; 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; use serde_json::Value;
// ─── 引入项目内部类型 ──────────────────────────────────────────────────────
// crate 表示"当前项目crate"。
// types 模块中定义了 LogEntry一条日志记录和 LogLevel日志级别如 INFO/ERROR
use crate::types::{LogEntry, LogLevel}; 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 { pub fn detect_json_log(line: &str) -> bool {
// matches! 是 Rust 的一个宏macro用于简化 match 表达式。
// 完整写法等价于:
// match serde_json::from_str::<Value>(line) {
// Ok(Value::Object(_)) => true,
// _ => false,
// }
//
// 拆解这段代码:
//
// 1. serde_json::from_str::<Value>(line)
// 尝试将字符串 line 解析为 JSON 值Value 类型)。
// ::<Value> 是"泛型参数"turbofish 语法),告诉 from_str 我们想要 Value 类型。
// 返回 Result<Value, Error>:成功返回 Ok(Value),失败返回 Err(Error)。
//
// 2. Ok(Value::Object(_))
// 模式匹配如果解析成功Ok且结果是 JSON 对象Value::Object
// 则匹配成功。_ 是通配符,表示"不关心对象里面的具体内容"。
//
// 如果匹配到 Ok(Value::Object(_)) 返回 true否则返回 false。
matches!(serde_json::from_str::<Value>(line), Ok(Value::Object(_))) matches!(serde_json::from_str::<Value>(line), Ok(Value::Object(_)))
} }
// ─── parse_line 函数 ──────────────────────────────────────────────────────
// 将一行 JSON 文本解析为 LogEntry日志条目结构体。
//
// 参数: line: &str — 一行日志文本。
// 返回: Option<LogEntry> — 解析成功返回 Some(LogEntry),失败或不合法返回 None。
// Option 是 Rust 的可选类型Some(值) 表示有值None 表示没有值。
pub fn parse_line(line: &str) -> Option<LogEntry> { pub fn parse_line(line: &str) -> Option<LogEntry> {
// ─── 跳过空行 ──────────────────────────────────────────────────────────
// line.trim() 去除首尾空白字符(空格、制表符、换行符等)。
// .is_empty() 检查是否为空字符串。
// 如果去除空白后是空的,说明是空行,不需要解析,直接返回 None。
if line.trim().is_empty() { if line.trim().is_empty() {
return None; 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()?; let mut fields: HashMap<String, Value> = serde_json::from_str(line).ok()?;
// ─── 保存原始行内容 ──────────────────────────────────────────────────
// line.to_string() 将 &str字符串切片引用转换为 String拥有所有权的字符串
// 保存原始行是为了在 UI 中显示未经修改的原始日志内容。
let raw_line = line.to_string(); 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"] let timestamp = ["timestamp", "time", "ts", "@timestamp"]
.iter() .iter()
.find_map(|key| fields.remove(*key)) .find_map(|key| fields.remove(*key))
.and_then(|v| v.as_str().map(String::from)); .and_then(|v| v.as_str().map(String::from));
// ─── 提取日志级别字段 ──────────────────────────────────────────────
// 与时间戳提取类似,但多了一步:将字符串解析为 LogLevel 枚举。
let level = ["level", "lvl", "severity"] let level = ["level", "lvl", "severity"]
.iter() .iter()
.find_map(|key| fields.remove(*key)) .find_map(|key| fields.remove(*key))
.and_then(|v| v.as_str().map(String::from)) .and_then(|v| v.as_str().map(String::from))
// .map(|s| s.parse::<LogLevel>(...)) — 尝试将字符串解析为 LogLevel 枚举。
// parse::<LogLevel> 中的 ::<LogLevel> 是泛型参数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::<LogLevel>().unwrap_or_else(|e| match e {})); .map(|s| s.parse::<LogLevel>().unwrap_or_else(|e| match e {}));
// ─── 构建 LogEntry 并返回 ──────────────────────────────────────────
// 此时 fields HashMap 中还剩下未被提取的字段(如 message、自定义字段等
// timestamp 和 level 已经从 fields 中移除了(通过 remove
//
// Some(LogEntry { ... }) — 使用结构体字面量创建 LogEntry 实例,
// 并用 Some() 包裹表示"有值"。
Some(LogEntry { Some(LogEntry {
// line_number 设为 0由调用者如 parse_line_with_number设置正确的值。
line_number: 0, line_number: 0,
// 原始行内容。
raw_line, raw_line,
// 时间戳(可能为 None如果 JSON 中没有时间戳字段)。
timestamp, timestamp,
// 日志级别(可能为 None如果 JSON 中没有级别字段)。
level, level,
// 剩余的 JSON 字段(已移除 timestamp 和 level
fields, fields,
}) })
} }
// ─── parse_line_with_number 函数 ──────────────────────────────────────────
// 与 parse_line 相同,但可以指定行号。
// 这是一个"便捷函数"convenience function避免调用者手动修改 line_number。
//
// 参数:
// - line: &str — 一行日志文本。
// - line_number: u64 — 行号(无符号 64 位整数)。
// 返回: Option<LogEntry> — 解析成功返回 Some(LogEntry),失败返回 None。
pub fn parse_line_with_number(line: &str, line_number: u64) -> Option<LogEntry> { pub fn parse_line_with_number(line: &str, line_number: u64) -> Option<LogEntry> {
// 先调用 parse_line 解析行内容,然后通过 .map() 修改行号。
// .map() 对 Option 进行转换:如果是 Some(entry),执行闭包;如果是 None保持 None。
//
// |mut entry| { ... } — 闭包参数 mut entry 表示"可变的 LogEntry"。
// 需要 mut 是因为我们要修改 entry.line_number 字段。
parse_line(line).map(|mut entry| { parse_line(line).map(|mut entry| {
entry.line_number = line_number; entry.line_number = line_number;
entry entry
// 闭包的最后一条表达式就是返回值Rust 不需要 return 关键字)。
}) })
} }
// ─── 单元测试 ────────────────────────────────────────────────────────────────
// `#[cfg(test)]` 表示以下代码只在测试时编译。
// `mod tests` 定义一个名为 tests 的子模块,用于存放测试函数。
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
// `use super::*;` — 将父模块json.rs 中的公开函数)全部引入当前作用域。
use super::*; use super::*;
#[test] #[test]
// 测试:解析一个完整的 JSON 日志行,验证所有字段正确提取。
fn test_valid_json_log() { 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"}"#; let line = r#"{"level":"INFO","message":"hello","timestamp":"2024-01-01T00:00:00Z"}"#;
// 解析这一行unwrap() 断言解析成功。
let entry = parse_line(line).unwrap(); let entry = parse_line(line).unwrap();
// 验证原始行内容被正确保存。
assert_eq!(entry.raw_line, line); assert_eq!(entry.raw_line, line);
// 验证时间戳正确提取。Some(...) 表示有值。
assert_eq!(entry.timestamp, Some("2024-01-01T00:00:00Z".to_string())); assert_eq!(entry.timestamp, Some("2024-01-01T00:00:00Z".to_string()));
// 验证日志级别正确解析为 LogLevel::Info。
assert_eq!(entry.level, Some(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!( assert_eq!(
entry.fields.get("message"), entry.fields.get("message"),
Some(&Value::String("hello".to_string())) Some(&Value::String("hello".to_string()))
); );
// 验证行号默认为 0。
assert_eq!(entry.line_number, 0); assert_eq!(entry.line_number, 0);
} }
#[test] #[test]
// 测试JSON 中缺少 timestamp 和 level 字段时,应返回 None。
fn test_json_missing_fields() { fn test_json_missing_fields() {
let line = r#"{"message":"just a message"}"#; let line = r#"{"message":"just a message"}"#;
let entry = parse_line(line).unwrap(); let entry = parse_line(line).unwrap();
// 没有 timestamp 字段,应该为 None。
assert!(entry.timestamp.is_none()); assert!(entry.timestamp.is_none());
// 没有 level 字段,应该为 None。
assert!(entry.level.is_none()); assert!(entry.level.is_none());
// message 字段应该保留在 fields 中。
assert_eq!( assert_eq!(
entry.fields.get("message"), entry.fields.get("message"),
Some(&Value::String("just a message".to_string())) Some(&Value::String("just a message".to_string()))
@@ -75,79 +250,106 @@ mod tests {
} }
#[test] #[test]
// 测试:纯文本(非 JSON应该解析失败返回 None。
fn test_plain_text() { fn test_plain_text() {
// assert! 宏断言表达式为 true。parse_line 返回 None.is_none() 为 true。
assert!(parse_line("hello world").is_none()); assert!(parse_line("hello world").is_none());
} }
#[test] #[test]
// 测试:空行应该返回 None。
fn test_empty_line() { fn test_empty_line() {
assert!(parse_line("").is_none()); assert!(parse_line("").is_none());
} }
#[test] #[test]
// 测试:只有空白字符的行应该返回 None。
fn test_whitespace_line() { fn test_whitespace_line() {
assert!(parse_line(" ").is_none()); assert!(parse_line(" ").is_none());
} }
#[test] #[test]
// 测试JSON 数组不是日志行detect_json_log 应返回 false
fn test_json_array() { fn test_json_array() {
// assert!(!expr) 断言表达式为 false。
// detect_json_log("[1,2,3]") 返回 false数组不是对象取反后为 true。
assert!(!detect_json_log("[1,2,3]")); assert!(!detect_json_log("[1,2,3]"));
} }
#[test] #[test]
// 测试JSON 字符串不是日志行。
fn test_json_string() { fn test_json_string() {
assert!(!detect_json_log("\"hello\"")); assert!(!detect_json_log("\"hello\""));
} }
#[test] #[test]
// 测试JSON 数字不是日志行。
fn test_json_number() { fn test_json_number() {
assert!(!detect_json_log("42")); assert!(!detect_json_log("42"));
} }
#[test] #[test]
// 测试JSON null 不是日志行。
fn test_json_null() { fn test_json_null() {
assert!(!detect_json_log("null")); assert!(!detect_json_log("null"));
} }
#[test] #[test]
// 测试JSON 布尔值不是日志行。
fn test_json_bool() { fn test_json_bool() {
assert!(!detect_json_log("true")); assert!(!detect_json_log("true"));
} }
#[test] #[test]
// 测试:不同大小写的日志级别都应该正确解析。
// LogLevel 实现了不区分大小写的解析("info"、"INFO"、"Info" 都能识别)。
fn test_level_variants() { fn test_level_variants() {
// 小写 "info"
let info_line = r#"{"level":"info"}"#; let info_line = r#"{"level":"info"}"#;
assert_eq!(parse_line(info_line).unwrap().level, Some(LogLevel::Info)); assert_eq!(parse_line(info_line).unwrap().level, Some(LogLevel::Info));
// 大写 "ERROR"
let error_line = r#"{"level":"ERROR"}"#; let error_line = r#"{"level":"ERROR"}"#;
assert_eq!(parse_line(error_line).unwrap().level, Some(LogLevel::Error)); assert_eq!(parse_line(error_line).unwrap().level, Some(LogLevel::Error));
// 小写 "warn"
let warn_line = r#"{"level":"warn"}"#; let warn_line = r#"{"level":"warn"}"#;
assert_eq!(parse_line(warn_line).unwrap().level, Some(LogLevel::Warn)); assert_eq!(parse_line(warn_line).unwrap().level, Some(LogLevel::Warn));
} }
#[test] #[test]
// 测试所有候选时间戳键名timestamp, time, ts, @timestamp都能被识别。
fn test_timestamp_key_names() { fn test_timestamp_key_names() {
// &[] 创建一个数组的引用,然后遍历其中的每个字符串。
for key in &["timestamp", "time", "ts", "@timestamp"] { for key in &["timestamp", "time", "ts", "@timestamp"] {
// format! 宏用于格式化字符串,类似 Python 的 f-string。
// r#"..."# 中的 {key} 会被替换为变量 key 的值。
let line = format!(r#"{{"{key}":"2024-01-01T00:00:00Z"}}"#); let line = format!(r#"{{"{key}":"2024-01-01T00:00:00Z"}}"#);
// 注意format! 中的花括号需要双写 {{ }} 来表示字面量的花括号。
// 因为单花括号 { } 是格式化占位符。
let entry = parse_line(&line).unwrap(); let entry = parse_line(&line).unwrap();
assert_eq!( assert_eq!(
entry.timestamp, entry.timestamp,
Some("2024-01-01T00:00:00Z".to_string()), Some("2024-01-01T00:00:00Z".to_string()),
// 第三个参数是断言失败时的自定义错误信息。
"failed for key: {key}" "failed for key: {key}"
); );
} }
} }
#[test] #[test]
// 测试parse_line_with_number 能正确设置行号。
fn test_parse_line_with_number() { fn test_parse_line_with_number() {
let line = r#"{"level":"DEBUG","message":"test"}"#; let line = r#"{"level":"DEBUG","message":"test"}"#;
let entry = parse_line_with_number(line, 42).unwrap(); let entry = parse_line_with_number(line, 42).unwrap();
// 行号应该被设置为 42。
assert_eq!(entry.line_number, 42); assert_eq!(entry.line_number, 42);
// 日志级别也应该正确解析。
assert_eq!(entry.level, Some(LogLevel::Debug)); assert_eq!(entry.level, Some(LogLevel::Debug));
} }
#[test] #[test]
// 测试所有候选日志级别键名level, lvl, severity都能被识别。
fn test_level_key_variants() { fn test_level_key_variants() {
for key in &["level", "lvl", "severity"] { for key in &["level", "lvl", "severity"] {
let line = format!(r#"{{"{key}":"INFO"}}"#); let line = format!(r#"{{"{key}":"INFO"}}"#);

View File

@@ -1 +1,12 @@
// ─── parser 模块说明 ─────────────────────────────────────────────────────────
// 这个模块负责"解析"parse日志文件中的每一行内容。
// 日志可能有不同格式JSON、纯文本等parser 的职责是:
// 1. 识别一行的格式(是 JSON还是纯文本
// 2. 把这行内容解析成结构化的数据LogEntry 类型)
//
// 在 Rust 中mod.rs 是模块的入口文件,通过 `pub mod` 声明子模块。
// ──────────────────────────────────────────────────────────────────────────────
// 声明并导出 json 子模块(定义在 json.rs 文件中)。
// 该模块负责解析 JSON 格式的日志行。
pub mod json; pub mod json;