Compare commits
2 Commits
eedab3ac96
...
e9f75ce3b1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e9f75ce3b1 | ||
|
|
ef1889767a |
@@ -16,7 +16,14 @@
|
|||||||
// 类似于 Python 的 dict、JavaScript 的 Object/Map、Java 的 HashMap。
|
// 类似于 Python 的 dict、JavaScript 的 Object/Map、Java 的 HashMap。
|
||||||
// 它存储键值对(key-value pairs),可以通过键快速查找对应的值。
|
// 它存储键值对(key-value pairs),可以通过键快速查找对应的值。
|
||||||
// 这里用 HashMap<String, Value> 来存储 JSON 中除 timestamp/level 之外的其他字段。
|
// 这里用 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 处理库)。
|
// serde_json::Value — 来自 serde_json 库(Rust 中最流行的 JSON 处理库)。
|
||||||
// Value 是一个枚举类型,可以表示任意 JSON 值:
|
// Value 是一个枚举类型,可以表示任意 JSON 值:
|
||||||
@@ -32,7 +39,7 @@ use serde_json::Value;
|
|||||||
// ─── 引入项目内部类型 ──────────────────────────────────────────────────────
|
// ─── 引入项目内部类型 ──────────────────────────────────────────────────────
|
||||||
// crate 表示"当前项目(crate)"。
|
// crate 表示"当前项目(crate)"。
|
||||||
// types 模块中定义了 LogEntry(一条日志记录)和 LogLevel(日志级别,如 INFO/ERROR)。
|
// types 模块中定义了 LogEntry(一条日志记录)和 LogLevel(日志级别,如 INFO/ERROR)。
|
||||||
use crate::types::{LogEntry, LogLevel};
|
use crate::types::{DuplicateKey, LogEntry, LogLevel};
|
||||||
|
|
||||||
// ─── strip_bom 辅助函数 ──────────────────────────────────────────────────
|
// ─── strip_bom 辅助函数 ──────────────────────────────────────────────────
|
||||||
// 剥离行首的 UTF-8 BOM(Byte Order Mark, U+FEFF)。
|
// 剥离行首的 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(_)))
|
matches!(serde_json::from_str::<Value>(strip_bom(line)), Ok(Value::Object(_)))
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── take_string_field 辅助函数 ──────────────────────────────────────────────
|
// ─── DuplicateKeyVisitor ──────────────────────────────────────────────────
|
||||||
// 从 HashMap 中安全提取字符串字段。
|
// 自定义 serde Visitor,在反序列化 JSON 对象时检测重复 key。
|
||||||
//
|
//
|
||||||
// 遍历候选键名列表,找到第一个值为字符串类型的字段,移除并返回其值。
|
// 工作原理:
|
||||||
// 如果值不是字符串(如数字、布尔值等),保留该字段在 HashMap 中,继续尝试下一个候选键。
|
// serde 的 MapAccess trait 允许我们逐个遍历 JSON 对象的 key-value 对。
|
||||||
// 这样可以避免非字符串类型的字段被静默丢弃(数据丢失 bug)。
|
// 每读到一个 (key, value),我们:
|
||||||
|
// 1. 检查这个 key 是否已经见过(通过 HashSet)
|
||||||
|
// 2. 如果是重复 key,记录到 Vec<DuplicateKey> 中(包含所有出现过的值)
|
||||||
|
// 3. 将 key-value 插入 Map(last-wins,与 serde_json 默认行为一致)
|
||||||
//
|
//
|
||||||
// 参数:
|
// 这样既保持了兼容性(last-wins),又不丢失信息(所有值都记录在 DuplicateKey 中)。
|
||||||
// - fields: &mut HashMap<String, Value> — JSON 字段的 HashMap(可变引用)。
|
struct DuplicateKeyVisitor;
|
||||||
// - keys: &[&str] — 候选键名列表(按优先级排列)。
|
|
||||||
// 返回: Option<String> — 找到字符串值返回 Some(String),否则返回 None。
|
impl<'de> Visitor<'de> for DuplicateKeyVisitor {
|
||||||
fn take_string_field(fields: &mut HashMap<String, Value>, keys: &[&str]) -> Option<String> {
|
// 返回类型:(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 {
|
for key in keys {
|
||||||
// 先检查值是否为字符串类型(peek,不移除)。
|
if obj.get(*key).is_some_and(Value::is_string) {
|
||||||
// is_some_and(Value::is_string) 等价于:
|
let Some(Value::String(v)) = obj.remove(*key) else {
|
||||||
// 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 {
|
|
||||||
unreachable!("value was checked as string");
|
unreachable!("value was checked as string");
|
||||||
};
|
};
|
||||||
return Some(value);
|
return Some(v);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None
|
None
|
||||||
@@ -117,65 +175,34 @@ fn take_string_field(fields: &mut HashMap<String, Value>, keys: &[&str]) -> Opti
|
|||||||
// 返回: Option<LogEntry> — 解析成功返回 Some(LogEntry),失败或不合法返回 None。
|
// 返回: Option<LogEntry> — 解析成功返回 Some(LogEntry),失败或不合法返回 None。
|
||||||
// Option 是 Rust 的可选类型:Some(值) 表示有值,None 表示没有值。
|
// Option 是 Rust 的可选类型:Some(值) 表示有值,None 表示没有值。
|
||||||
pub fn parse_line(line: &str) -> Option<LogEntry> {
|
pub fn parse_line(line: &str) -> Option<LogEntry> {
|
||||||
// ─── 跳过空行 ──────────────────────────────────────────────────────────
|
|
||||||
let line = strip_bom(line);
|
let line = strip_bom(line);
|
||||||
|
|
||||||
// line.trim() 去除首尾空白字符(空格、制表符、换行符等)。
|
|
||||||
// .is_empty() 检查是否为空字符串。
|
|
||||||
// 如果去除空白后是空的,说明是空行,不需要解析,直接返回 None。
|
|
||||||
if line.trim().is_empty() {
|
if line.trim().is_empty() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── 解析 JSON 为 HashMap ──────────────────────────────────────────────
|
// ─── 使用自定义 Visitor 解析 JSON ──────────────────────────────────
|
||||||
// serde_json::from_str(line) 尝试将字符串解析为 JSON。
|
// 通过 DuplicateKeyVisitor 反序列化,在保持 last-wins 的同时检测重复 key。
|
||||||
// 由于我们声明了 HashMap<String, Value> 类型,Rust 会自动将 JSON 对象
|
// 返回的 (serde_json::Map, Vec<DuplicateKey>) 中:
|
||||||
// 转换为 HashMap,其中每个键是 String,每个值是 serde_json::Value。
|
// - Map 包含所有 key-value(重复 key 取最后一个值)
|
||||||
//
|
// - Vec 记录了所有重复 key 及其出现过的全部值
|
||||||
// .ok() 将 Result 转换为 Option:
|
let (mut obj, duplicate_keys) = parse_json_object_with_duplicates(line)?;
|
||||||
// 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();
|
let raw_line = line.to_string();
|
||||||
|
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"])
|
||||||
// 使用 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"])
|
|
||||||
.map(|s| s.parse::<LogLevel>().unwrap_or_else(|e| match e {}));
|
.map(|s| s.parse::<LogLevel>().unwrap_or_else(|e| match e {}));
|
||||||
|
|
||||||
// ─── 构建 LogEntry 并返回 ──────────────────────────────────────────
|
// serde_json::Map → HashMap:剩余字段转为 HashMap 存入 fields
|
||||||
// 此时 fields HashMap 中还剩下未被提取的字段(如 message、自定义字段等)。
|
let fields: HashMap<String, Value> = obj.into_iter().collect();
|
||||||
// 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,
|
||||||
|
duplicate_keys,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -319,6 +346,13 @@ mod tests {
|
|||||||
assert_eq!(parse_line(warn_line).unwrap().level, Some(LogLevel::Warn));
|
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]
|
#[test]
|
||||||
// 测试:所有候选时间戳键名(timestamp, time, ts, @timestamp)都能被识别。
|
// 测试:所有候选时间戳键名(timestamp, time, ts, @timestamp)都能被识别。
|
||||||
fn test_timestamp_key_names() {
|
fn test_timestamp_key_names() {
|
||||||
@@ -456,4 +490,77 @@ mod tests {
|
|||||||
Some(&Value::String("\u{FEFF}hello".into()))
|
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");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,13 +71,17 @@ impl FromStr for LogLevel {
|
|||||||
// 接收一个字符串切片 &str,返回 Result<LogLevel, Infallible>。
|
// 接收一个字符串切片 &str,返回 Result<LogLevel, Infallible>。
|
||||||
// 由于 Err 类型是 Infallible,实际上返回值总是 Ok(LogLevel)。
|
// 由于 Err 类型是 Infallible,实际上返回值总是 Ok(LogLevel)。
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
// `s.to_uppercase()` — 将字符串转换为大写,实现不区分大小写的匹配。
|
// `s.trim()` — 去除字符串前后的 Unicode 空白字符。
|
||||||
|
// 例如 " WARN " → "WARN","\tINFO\n" → "INFO"。
|
||||||
|
let trimmed = s.trim();
|
||||||
|
|
||||||
|
// `trimmed.to_uppercase()` — 将 trimmed 后的字符串转换为大写,实现不区分大小写的匹配。
|
||||||
// 例如 "info"、"Info"、"INFO" 都会被转换为 "INFO"。
|
// 例如 "info"、"Info"、"INFO" 都会被转换为 "INFO"。
|
||||||
// 返回一个新的 String(堆分配)。
|
// 返回一个新的 String(堆分配)。
|
||||||
//
|
//
|
||||||
// `.as_str()` — 将 String 转换回 &str(字符串切片引用)。
|
// `.as_str()` — 将 String 转换回 &str(字符串切片引用)。
|
||||||
// 因为 match 需要匹配 &str 而不是 String。
|
// 因为 match 需要匹配 &str 而不是 String。
|
||||||
match s.to_uppercase().as_str() {
|
match trimmed.to_uppercase().as_str() {
|
||||||
// `|` 在 match 分支中表示"或"(multiple patterns)。
|
// `|` 在 match 分支中表示"或"(multiple patterns)。
|
||||||
// "ERROR" | "ERR" | "SEVERE" | "FATAL" 都匹配到 LogLevel::Error。
|
// "ERROR" | "ERR" | "SEVERE" | "FATAL" 都匹配到 LogLevel::Error。
|
||||||
"ERROR" | "ERR" | "SEVERE" | "FATAL" => Ok(LogLevel::Error),
|
"ERROR" | "ERR" | "SEVERE" | "FATAL" => Ok(LogLevel::Error),
|
||||||
@@ -86,9 +90,9 @@ impl FromStr for LogLevel {
|
|||||||
"DEBUG" | "DBG" => Ok(LogLevel::Debug),
|
"DEBUG" | "DBG" => Ok(LogLevel::Debug),
|
||||||
"TRACE" | "TRC" => Ok(LogLevel::Trace),
|
"TRACE" | "TRC" => Ok(LogLevel::Trace),
|
||||||
// `_` 是通配符,匹配所有未被上面分支捕获的值。
|
// `_` 是通配符,匹配所有未被上面分支捕获的值。
|
||||||
// 对于未知级别,包装为 Unknown 并保存原始字符串。
|
// 对于未知级别,包装为 Unknown 并保存 trimmed 后的字符串。
|
||||||
// s.to_string() 将 &str 转换为 String(注意这里用原始的 s,不是大写后的)。
|
// s.to_string() 将 &str 转换为 String(注意这里用 trimmed,不是原始 s)。
|
||||||
_ => Ok(LogLevel::Unknown(s.to_string())),
|
_ => Ok(LogLevel::Unknown(trimmed.to_string())),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -119,6 +123,18 @@ impl fmt::Display for LogLevel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ─── LogEntry 结构体 ────────────────────────────────────────────────────────
|
// ─── 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<Value>,
|
||||||
|
}
|
||||||
|
|
||||||
/// 一行解析后的日志
|
/// 一行解析后的日志
|
||||||
///
|
///
|
||||||
/// 表示日志文件中经过解析器处理后的一行内容。
|
/// 表示日志文件中经过解析器处理后的一行内容。
|
||||||
@@ -144,6 +160,9 @@ pub struct LogEntry {
|
|||||||
/// HashMap<String, Value> 是一个字典,键是字段名,值是 JSON 值。
|
/// HashMap<String, Value> 是一个字典,键是字段名,值是 JSON 值。
|
||||||
/// 例如 {"message": "hello", "request_id": "abc123"}。
|
/// 例如 {"message": "hello", "request_id": "abc123"}。
|
||||||
pub fields: HashMap<String, Value>,
|
pub fields: HashMap<String, Value>,
|
||||||
|
|
||||||
|
/// JSON 中重复出现的 key 记录(正常日志为空 Vec)
|
||||||
|
pub duplicate_keys: Vec<DuplicateKey>,
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── SearchResult 结构体 ────────────────────────────────────────────────────
|
// ─── SearchResult 结构体 ────────────────────────────────────────────────────
|
||||||
@@ -288,6 +307,34 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_from_str_whitespace_trimmed() {
|
||||||
|
assert_eq!(" WARN ".parse::<LogLevel>(), Ok(LogLevel::Warn));
|
||||||
|
assert_eq!("\tINFO".parse::<LogLevel>(), Ok(LogLevel::Info));
|
||||||
|
assert_eq!("ERROR\n".parse::<LogLevel>(), Ok(LogLevel::Error));
|
||||||
|
assert_eq!(" debug ".parse::<LogLevel>(), Ok(LogLevel::Debug));
|
||||||
|
assert_eq!("\tTRACE\t".parse::<LogLevel>(), Ok(LogLevel::Trace));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_from_str_whitespace_unknown_trimmed() {
|
||||||
|
// Unknown stores the trimmed value, not the original.
|
||||||
|
assert_eq!(
|
||||||
|
" CUSTOM ".parse::<LogLevel>(),
|
||||||
|
Ok(LogLevel::Unknown("CUSTOM".into()))
|
||||||
|
);
|
||||||
|
// Pure whitespace becomes Unknown("").
|
||||||
|
assert_eq!(
|
||||||
|
" ".parse::<LogLevel>(),
|
||||||
|
Ok(LogLevel::Unknown("".into()))
|
||||||
|
);
|
||||||
|
// Internal whitespace is NOT collapsed.
|
||||||
|
assert_eq!(
|
||||||
|
"W ARN".parse::<LogLevel>(),
|
||||||
|
Ok(LogLevel::Unknown("W ARN".into()))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
// 测试:LogLevel 的 Display 输出格式是否正确。
|
// 测试:LogLevel 的 Display 输出格式是否正确。
|
||||||
fn test_display_output() {
|
fn test_display_output() {
|
||||||
@@ -319,6 +366,7 @@ mod tests {
|
|||||||
timestamp: Some("2024-01-01T00:00:00".to_string()),
|
timestamp: Some("2024-01-01T00:00:00".to_string()),
|
||||||
level: Some(LogLevel::Info),
|
level: Some(LogLevel::Info),
|
||||||
fields,
|
fields,
|
||||||
|
duplicate_keys: vec![],
|
||||||
};
|
};
|
||||||
|
|
||||||
// 逐字段验证。
|
// 逐字段验证。
|
||||||
|
|||||||
Reference in New Issue
Block a user