use std::collections::HashMap;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct Event {
pub timestamp: Option<DateTime<Utc>>,
pub level: Option<String>,
pub message: Option<String>,
pub fields: HashMap<String, FieldValue>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum FieldValue {
String(String),
Number(f64),
Boolean(bool),
Null,
}
impl Event {
pub fn new() -> Self {
Self::default()
}
pub fn set_field(&mut self, key: String, value: FieldValue) {
self.fields.insert(key, value);
}
pub fn filter_keys(&mut self, keys: &[String]) {
let mut new_fields = HashMap::new();
let mut keep_timestamp = false;
let mut keep_level = false;
let mut keep_message = false;
for key in keys {
match key.as_str() {
"timestamp" | "ts" | "time" | "at" | "_t" | "@t" | "t" => {
if self.timestamp.is_some() {
keep_timestamp = true;
}
}
"level" | "log_level" | "loglevel" | "lvl" | "severity" | "@l" => {
if self.level.is_some() {
keep_level = true;
}
}
"message" | "msg" | "@m" => {
if self.message.is_some() {
keep_message = true;
}
}
_ => {
if let Some(value) = self.fields.get(key) {
new_fields.insert(key.clone(), value.clone());
}
}
}
}
if !keep_timestamp {
self.timestamp = None;
}
if !keep_level {
self.level = None;
}
if !keep_message {
self.message = None;
}
self.fields = new_fields;
}
pub fn extract_core_fields(&mut self) {
for ts_key in &["timestamp", "ts", "time", "at", "_t", "@t", "t"] {
if let Some(FieldValue::String(ts_str)) = self.fields.get(*ts_key) {
if let Ok(ts) = parse_timestamp(ts_str) {
self.timestamp = Some(ts);
break;
}
}
}
for level_key in &["level", "log_level", "loglevel", "lvl", "severity", "@l"] {
if let Some(value) = self.fields.get(*level_key) {
if let Some(level_str) = value.as_string() {
self.level = Some(level_str.clone());
break;
}
}
}
for msg_key in &["message", "msg", "@m"] {
if let Some(value) = self.fields.get(*msg_key) {
if let Some(msg_str) = value.as_string() {
self.message = Some(msg_str.clone());
break;
}
}
}
}
pub fn has_displayable_content(&self) -> bool {
self.timestamp.is_some() ||
self.level.is_some() ||
self.message.is_some() ||
!self.fields.is_empty()
}
}
impl FieldValue {
pub fn as_string(&self) -> Option<&String> {
match self {
FieldValue::String(s) => Some(s),
_ => None,
}
}
}
impl std::fmt::Display for FieldValue {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
FieldValue::String(s) => write!(f, "{}", s),
FieldValue::Number(n) => write!(f, "{}", n),
FieldValue::Boolean(b) => write!(f, "{}", b),
FieldValue::Null => write!(f, "null"),
}
}
}
fn parse_timestamp(ts_str: &str) -> Result<DateTime<Utc>, chrono::ParseError> {
let formats = [
"%Y-%m-%dT%H:%M:%S%.fZ", "%Y-%m-%dT%H:%M:%SZ", "%Y-%m-%dT%H:%M:%S%.f%:z", "%Y-%m-%dT%H:%M:%S%:z", "%Y-%m-%d %H:%M:%S%.f", "%Y-%m-%d %H:%M:%S", "%b %d %H:%M:%S", ];
for format in &formats {
if let Ok(dt) = DateTime::parse_from_str(ts_str, format) {
return Ok(dt.with_timezone(&Utc));
}
if let Ok(dt) = chrono::NaiveDateTime::parse_from_str(ts_str, format) {
return Ok(dt.and_utc());
}
}
chrono::NaiveDateTime::parse_from_str(ts_str, "%Y-%m-%dT%H:%M:%SZ")
.map(|dt| dt.and_utc())
}