fix(parser): trim whitespace in LogLevel::from_str to prevent misclassification (closes #21)

Before this fix, level strings with surrounding whitespace (e.g. " WARN ")
were incorrectly parsed as Unknown instead of the matching variant.
This primarily affected the JSON log path where serde preserves the raw
field value including whitespace.

Changes:
- Add s.trim() before case-insensitive matching in FromStr impl
- Store trimmed value in Unknown variant to avoid whitespace noise
- Add unit tests for whitespace-padded known levels
- Add unit tests for Unknown trimming semantics (empty, internal ws)
- Add JSON regression test for level field with surrounding whitespace
This commit is contained in:
dailz
2026-06-10 13:37:11 +08:00
parent eedab3ac96
commit ef1889767a
2 changed files with 44 additions and 5 deletions

View File

@@ -319,6 +319,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() {

View File

@@ -71,13 +71,17 @@ impl FromStr for LogLevel {
// 接收一个字符串切片 &str返回 Result<LogLevel, Infallible>。
// 由于 Err 类型是 Infallible实际上返回值总是 Ok(LogLevel)。
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"。
// 返回一个新的 String堆分配
//
// `.as_str()` — 将 String 转换回 &str字符串切片引用
// 因为 match 需要匹配 &str 而不是 String。
match s.to_uppercase().as_str() {
match trimmed.to_uppercase().as_str() {
// `|` 在 match 分支中表示"或"multiple patterns
// "ERROR" | "ERR" | "SEVERE" | "FATAL" 都匹配到 LogLevel::Error。
"ERROR" | "ERR" | "SEVERE" | "FATAL" => Ok(LogLevel::Error),
@@ -86,9 +90,9 @@ impl FromStr for LogLevel {
"DEBUG" | "DBG" => Ok(LogLevel::Debug),
"TRACE" | "TRC" => Ok(LogLevel::Trace),
// `_` 是通配符,匹配所有未被上面分支捕获的值。
// 对于未知级别,包装为 Unknown 并保存原始字符串。
// s.to_string() 将 &str 转换为 String注意这里用原始 s,不是大写后的)。
_ => Ok(LogLevel::Unknown(s.to_string())),
// 对于未知级别,包装为 Unknown 并保存 trimmed 后的字符串。
// s.to_string() 将 &str 转换为 String注意这里用 trimmed不是原始 s
_ => Ok(LogLevel::Unknown(trimmed.to_string())),
}
}
}
@@ -288,6 +292,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]
// 测试LogLevel 的 Display 输出格式是否正确。
fn test_display_output() {