1use chrono::{DateTime, Utc};
32use serde::Serialize;
33use std::path::Path;
34
35const ECS_VERSION: &str = "1.12.1";
37
38#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
42pub struct Event<'a> {
43 #[serde(rename = "@timestamp")]
47 pub timestamp: DateTime<Utc>,
48
49 #[serde(rename = "log.level")]
53 pub log_level: &'static str,
54
55 pub message: String,
59
60 #[serde(rename = "ecs.version")]
64 pub ecs_version: &'static str,
65
66 #[serde(rename = "log.origin")]
70 pub log_origin: LogOrigin<'a>,
71}
72
73#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
77pub struct LogOrigin<'a> {
78 pub file: LogOriginFile<'a>,
82
83 pub rust: LogOriginRust<'a>,
87}
88
89#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
93pub struct LogOriginFile<'a> {
94 #[serde(skip_serializing_if = "Option::is_none")]
98 pub line: Option<u32>,
99
100 #[serde(skip_serializing_if = "Option::is_none")]
104 pub name: Option<&'a str>,
105}
106
107#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
109pub struct LogOriginRust<'a> {
110 pub target: &'a str,
114
115 #[serde(skip_serializing_if = "Option::is_none")]
119 pub module_path: Option<&'a str>,
120
121 #[serde(skip_serializing_if = "Option::is_none")]
125 pub file_path: Option<&'a str>,
126}
127
128impl<'a> Event<'a> {
129 pub fn new(timestamp: DateTime<Utc>, record: &'a log::Record<'a>) -> Self {
131 let file_path = record.file().map(Path::new);
132
133 Event {
134 timestamp,
135 log_level: record.level().as_str(),
136 message: record.args().to_string(),
137 ecs_version: ECS_VERSION,
138 log_origin: LogOrigin {
139 file: LogOriginFile {
140 line: record.line(),
141 name: file_path
142 .and_then(|p| p.file_name())
143 .and_then(|os_str| os_str.to_str()),
144 },
145 rust: LogOriginRust {
146 target: record.target(),
147 module_path: record.module_path(),
148 file_path: record.file(),
149 },
150 },
151 }
152 }
153}
154
155#[cfg(test)]
156mod tests {
157 use super::*;
158
159 #[test]
160 fn test_from_log_record() {
161 let timestamp = DateTime::parse_from_rfc3339("2021-11-27T07:18:11.712009300Z")
162 .unwrap()
163 .with_timezone(&Utc);
164
165 let record = log::Record::builder()
166 .args(format_args!("Error!"))
167 .level(log::Level::Error)
168 .target("myApp")
169 .file(Some("src/server.rs"))
170 .line(Some(144))
171 .module_path(Some("my_app::server"))
172 .build();
173
174 let event = Event::new(timestamp, &record);
175
176 assert_eq!(
177 event,
178 Event {
179 timestamp,
180 log_level: "ERROR",
181 message: "Error!".to_string(),
182 ecs_version: "1.12.1",
183 log_origin: LogOrigin {
184 file: LogOriginFile {
185 line: Some(144),
186 name: Some("server.rs")
187 },
188 rust: LogOriginRust {
189 target: "myApp",
190 module_path: Some("my_app::server"),
191 file_path: Some("src/server.rs")
192 }
193 }
194 }
195 );
196 }
197
198 #[test]
199 fn test_serialize() {
200 let timestamp = DateTime::parse_from_rfc3339("2021-11-24T17:38:21.000098765Z")
201 .unwrap()
202 .with_timezone(&Utc);
203
204 let event = Event {
205 timestamp,
206 log_level: "TRACE",
207 message: "tracing msg".to_string(),
208 ecs_version: "1.12.1",
209 log_origin: LogOrigin {
210 file: LogOriginFile {
211 line: Some(1234),
212 name: Some("file.rs"),
213 },
214 rust: LogOriginRust {
215 target: "myCustomTarget123",
216 module_path: Some("my_app::path::to::your::file"),
217 file_path: Some("src/path/to/your/file.rs"),
218 },
219 },
220 };
221
222 assert_eq!(
223 serde_json::to_string(&event).expect("Failed to serialize ECS event"),
224 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"}}}"#
225 );
226 }
227
228 #[test]
229 fn test_serialize_with_none() {
230 let timestamp = DateTime::parse_from_rfc3339("2021-11-24T17:38:21.000098765Z")
231 .unwrap()
232 .with_timezone(&Utc);
233
234 let event = Event {
235 timestamp,
236 log_level: "TRACE",
237 message: "tracing msg".to_string(),
238 ecs_version: "1.12.1",
239 log_origin: LogOrigin {
240 file: LogOriginFile {
241 line: None,
242 name: None,
243 },
244 rust: LogOriginRust {
245 target: "myCustomTarget123",
246 module_path: None,
247 file_path: None,
248 },
249 },
250 };
251
252 assert_eq!(
253 serde_json::to_string(&event).expect("Failed to serialize ECS event"),
254 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"}}}"#
255 );
256 }
257}