use chrono::{DateTime, Utc};
use serde::Serialize;
use std::path::Path;
const ECS_VERSION: &str = "1.12.1";
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct Event<'a> {
#[serde(rename = "@timestamp")]
pub timestamp: DateTime<Utc>,
#[serde(rename = "log.level")]
pub log_level: &'static str,
pub message: String,
#[serde(rename = "ecs.version")]
pub ecs_version: &'static str,
#[serde(rename = "log.origin")]
pub log_origin: LogOrigin<'a>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct LogOrigin<'a> {
pub file: LogOriginFile<'a>,
pub rust: LogOriginRust<'a>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct LogOriginFile<'a> {
#[serde(skip_serializing_if = "Option::is_none")]
pub line: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<&'a str>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct LogOriginRust<'a> {
pub target: &'a str,
#[serde(skip_serializing_if = "Option::is_none")]
pub module_path: Option<&'a str>,
#[serde(skip_serializing_if = "Option::is_none")]
pub file_path: Option<&'a str>,
}
impl<'a> Event<'a> {
pub fn new(timestamp: DateTime<Utc>, record: &'a log::Record<'a>) -> Self {
let file_path = record.file().map(Path::new);
Event {
timestamp,
log_level: record.level().as_str(),
message: record.args().to_string(),
ecs_version: ECS_VERSION,
log_origin: LogOrigin {
file: LogOriginFile {
line: record.line(),
name: file_path
.and_then(|p| p.file_name())
.and_then(|os_str| os_str.to_str()),
},
rust: LogOriginRust {
target: record.target(),
module_path: record.module_path(),
file_path: record.file(),
},
},
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_from_log_record() {
let timestamp = DateTime::parse_from_rfc3339("2021-11-27T07:18:11.712009300Z")
.unwrap()
.with_timezone(&Utc);
let record = log::Record::builder()
.args(format_args!("Error!"))
.level(log::Level::Error)
.target("myApp")
.file(Some("src/server.rs"))
.line(Some(144))
.module_path(Some("my_app::server"))
.build();
let event = Event::new(timestamp, &record);
assert_eq!(
event,
Event {
timestamp,
log_level: "ERROR",
message: "Error!".to_string(),
ecs_version: "1.12.1",
log_origin: LogOrigin {
file: LogOriginFile {
line: Some(144),
name: Some("server.rs")
},
rust: LogOriginRust {
target: "myApp",
module_path: Some("my_app::server"),
file_path: Some("src/server.rs")
}
}
}
);
}
#[test]
fn test_serialize() {
let timestamp = DateTime::parse_from_rfc3339("2021-11-24T17:38:21.000098765Z")
.unwrap()
.with_timezone(&Utc);
let event = Event {
timestamp,
log_level: "TRACE",
message: "tracing msg".to_string(),
ecs_version: "1.12.1",
log_origin: LogOrigin {
file: LogOriginFile {
line: Some(1234),
name: Some("file.rs"),
},
rust: LogOriginRust {
target: "myCustomTarget123",
module_path: Some("my_app::path::to::your::file"),
file_path: Some("src/path/to/your/file.rs"),
},
},
};
assert_eq!(
serde_json::to_string(&event).expect("Failed to serialize ECS event"),
r#"{"@timestamp":"2021-11-24T17:38:21.000098765Z","log.level":"TRACE","message":"tracing msg","ecs.version":"1.12.1","log.origin":{"file":{"line":1234,"name":"file.rs"},"rust":{"target":"myCustomTarget123","module_path":"my_app::path::to::your::file","file_path":"src/path/to/your/file.rs"}}}"#
);
}
#[test]
fn test_serialize_with_none() {
let timestamp = DateTime::parse_from_rfc3339("2021-11-24T17:38:21.000098765Z")
.unwrap()
.with_timezone(&Utc);
let event = Event {
timestamp,
log_level: "TRACE",
message: "tracing msg".to_string(),
ecs_version: "1.12.1",
log_origin: LogOrigin {
file: LogOriginFile {
line: None,
name: None,
},
rust: LogOriginRust {
target: "myCustomTarget123",
module_path: None,
file_path: None,
},
},
};
assert_eq!(
serde_json::to_string(&event).expect("Failed to serialize ECS event"),
r#"{"@timestamp":"2021-11-24T17:38:21.000098765Z","log.level":"TRACE","message":"tracing msg","ecs.version":"1.12.1","log.origin":{"file":{},"rust":{"target":"myCustomTarget123"}}}"#
);
}
}