feat(core): implement JSON log parser
This commit is contained in:
@@ -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!()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user