feat(core): implement JSON log parser

This commit is contained in:
dailz
2026-04-10 23:08:26 +08:00
parent f173adc018
commit 37bebc1a26

View File

@@ -1,8 +1,158 @@
use crate::types::LogEntry;
use std::collections::HashMap;
pub fn parse_line(_line: &str) -> Option<LogEntry> {
todo!()
use serde_json::Value;
use crate::types::{LogEntry, LogLevel};
pub fn detect_json_log(line: &str) -> bool {
matches!(serde_json::from_str::<Value>(line), Ok(Value::Object(_)))
}
pub fn parse_line(line: &str) -> Option<LogEntry> {
if line.trim().is_empty() {
return None;
}
let mut fields: HashMap<String, Value> = serde_json::from_str(line).ok()?;
let raw_line = line.to_string();
let timestamp = ["timestamp", "time", "ts", "@timestamp"]
.iter()
.find_map(|key| fields.remove(*key))
.and_then(|v| v.as_str().map(String::from));
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>().unwrap());
Some(LogEntry {
line_number: 0,
raw_line,
timestamp,
level,
fields,
})
}
pub fn parse_line_with_number(line: &str, line_number: u64) -> Option<LogEntry> {
parse_line(line).map(|mut entry| {
entry.line_number = line_number;
entry
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_valid_json_log() {
let line = r#"{"level":"INFO","message":"hello","timestamp":"2024-01-01T00:00:00Z"}"#;
let entry = parse_line(line).unwrap();
assert_eq!(entry.raw_line, line);
assert_eq!(entry.timestamp, Some("2024-01-01T00:00:00Z".to_string()));
assert_eq!(entry.level, Some(LogLevel::Info));
assert_eq!(
entry.fields.get("message"),
Some(&Value::String("hello".to_string()))
);
assert_eq!(entry.line_number, 0);
}
#[test]
fn test_json_missing_fields() {
let line = r#"{"message":"just a message"}"#;
let entry = parse_line(line).unwrap();
assert!(entry.timestamp.is_none());
assert!(entry.level.is_none());
assert_eq!(
entry.fields.get("message"),
Some(&Value::String("just a message".to_string()))
);
}
#[test]
fn test_plain_text() {
assert!(parse_line("hello world").is_none());
}
#[test]
fn test_empty_line() {
assert!(parse_line("").is_none());
}
#[test]
fn test_whitespace_line() {
assert!(parse_line(" ").is_none());
}
#[test]
fn test_json_array() {
assert!(!detect_json_log("[1,2,3]"));
}
#[test]
fn test_json_string() {
assert!(!detect_json_log("\"hello\""));
}
#[test]
fn test_json_number() {
assert!(!detect_json_log("42"));
}
#[test]
fn test_json_null() {
assert!(!detect_json_log("null"));
}
#[test]
fn test_json_bool() {
assert!(!detect_json_log("true"));
}
#[test]
fn test_level_variants() {
let info_line = r#"{"level":"info"}"#;
assert_eq!(parse_line(info_line).unwrap().level, Some(LogLevel::Info));
let error_line = r#"{"level":"ERROR"}"#;
assert_eq!(parse_line(error_line).unwrap().level, Some(LogLevel::Error));
let warn_line = r#"{"level":"warn"}"#;
assert_eq!(parse_line(warn_line).unwrap().level, Some(LogLevel::Warn));
}
#[test]
fn test_timestamp_key_names() {
for key in &["timestamp", "time", "ts", "@timestamp"] {
let line = format!(r#"{{"{key}":"2024-01-01T00:00:00Z"}}"#);
let entry = parse_line(&line).unwrap();
assert_eq!(
entry.timestamp,
Some("2024-01-01T00:00:00Z".to_string()),
"failed for key: {key}"
);
}
}
#[test]
fn test_parse_line_with_number() {
let line = r#"{"level":"DEBUG","message":"test"}"#;
let entry = parse_line_with_number(line, 42).unwrap();
assert_eq!(entry.line_number, 42);
assert_eq!(entry.level, Some(LogLevel::Debug));
}
#[test]
fn test_level_key_variants() {
for key in &["level", "lvl", "severity"] {
let line = format!(r#"{{"{key}":"INFO"}}"#);
let entry = parse_line(&line).unwrap();
assert_eq!(entry.level, Some(LogLevel::Info), "failed for key: {key}");
}
}
pub fn detect_json_log(_line: &str) -> bool {
todo!()
}