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:
@@ -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;
|
||||
|
||||
// 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::<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(_)))
|
||||
}
|
||||
|
||||
// ─── parse_line 函数 ──────────────────────────────────────────────────────
|
||||
// 将一行 JSON 文本解析为 LogEntry(日志条目)结构体。
|
||||
//
|
||||
// 参数: line: &str — 一行日志文本。
|
||||
// 返回: Option<LogEntry> — 解析成功返回 Some(LogEntry),失败或不合法返回 None。
|
||||
// Option 是 Rust 的可选类型:Some(值) 表示有值,None 表示没有值。
|
||||
pub fn parse_line(line: &str) -> Option<LogEntry> {
|
||||
// ─── 跳过空行 ──────────────────────────────────────────────────────────
|
||||
// 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()?;
|
||||
|
||||
// ─── 保存原始行内容 ──────────────────────────────────────────────────
|
||||
// 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>(...)) — 尝试将字符串解析为 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 {}));
|
||||
|
||||
// ─── 构建 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<LogEntry> — 解析成功返回 Some(LogEntry),失败返回 None。
|
||||
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| {
|
||||
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"}}"#);
|
||||
|
||||
@@ -1 +1,12 @@
|
||||
// ─── parser 模块说明 ─────────────────────────────────────────────────────────
|
||||
// 这个模块负责"解析"(parse)日志文件中的每一行内容。
|
||||
// 日志可能有不同格式(JSON、纯文本等),parser 的职责是:
|
||||
// 1. 识别一行的格式(是 JSON?还是纯文本?)
|
||||
// 2. 把这行内容解析成结构化的数据(LogEntry 类型)
|
||||
//
|
||||
// 在 Rust 中,mod.rs 是模块的入口文件,通过 `pub mod` 声明子模块。
|
||||
// ──────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
// 声明并导出 json 子模块(定义在 json.rs 文件中)。
|
||||
// 该模块负责解析 JSON 格式的日志行。
|
||||
pub mod json;
|
||||
|
||||
Reference in New Issue
Block a user