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> {
|
use serde_json::Value;
|
||||||
todo!()
|
|
||||||
|
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 detect_json_log(_line: &str) -> bool {
|
|
||||||
todo!()
|
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}");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user