ecs_logger/
ecs.rs

1//! Models which represent Elastic Common Schema (ECS) event and its fields
2//!
3//! The event follows [ECS Logging spec](https://github.com/elastic/ecs-logging/tree/master/spec).
4//!
5//! ## Example
6//!
7//! ```
8//! use ecs_logger::ecs::{Event, LogOrigin, LogOriginFile, LogOriginRust};
9//!
10//! let event = Event {
11//!     timestamp: chrono::Utc::now(),
12//!     log_level: "ERROR",
13//!     message: "Error!".to_string(),
14//!     ecs_version: "1.12.1",
15//!     log_origin: LogOrigin {
16//!         file: LogOriginFile {
17//!             line: Some(144),
18//!             name: Some("server.rs"),
19//!         },
20//!         rust: LogOriginRust {
21//!             target: "myApp",
22//!             module_path: Some("my_app::server"),
23//!             file_path: Some("src/server.rs"),
24//!         },
25//!     },
26//! };
27//!
28//! println!("{}", serde_json::to_string(&event).unwrap());
29//! ```
30
31use chrono::{DateTime, Utc};
32use serde::Serialize;
33use std::path::Path;
34
35/// Represents Elastic Common Schema version.
36const ECS_VERSION: &str = "1.12.1";
37
38/// Representation of an event compatible with ECS logging.
39///
40/// The event follows [ECS Logging spec](https://github.com/elastic/ecs-logging/tree/master/spec).
41#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
42pub struct Event<'a> {
43    /// Date and time when the message is logged.
44    ///
45    /// Mapped to `@timestamp` field.
46    #[serde(rename = "@timestamp")]
47    pub timestamp: DateTime<Utc>,
48
49    /// The verbosity level of the message.
50    ///
51    /// Mapped to `log.level` field.
52    #[serde(rename = "log.level")]
53    pub log_level: &'static str,
54
55    /// The message body.
56    ///
57    /// Mapped to `message` field.
58    pub message: String,
59
60    /// ECS version this event conforms to.
61    ///
62    /// Mapped to `ecs.version` field.
63    #[serde(rename = "ecs.version")]
64    pub ecs_version: &'static str,
65
66    /// Information about the source code which logged the message.
67    ///
68    /// Mapped to `log.origin` field.
69    #[serde(rename = "log.origin")]
70    pub log_origin: LogOrigin<'a>,
71}
72
73/// Information about the source code which logged the message.
74///
75/// <https://www.elastic.co/guide/en/ecs/current/ecs-log.html>
76#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
77pub struct LogOrigin<'a> {
78    /// Representation of the source code which logged the message.
79    ///
80    /// Mapped to `log.origin.file` field.
81    pub file: LogOriginFile<'a>,
82
83    /// Rust-specific information about the source code which logged the message.
84    ///
85    /// Mapped to `log.origin.rust` field.
86    pub rust: LogOriginRust<'a>,
87}
88
89/// Representation of the source code which logged the message.
90///
91/// <https://www.elastic.co/guide/en/ecs/current/ecs-log.html>
92#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
93pub struct LogOriginFile<'a> {
94    /// The line number of the source code which logged the message.
95    ///
96    /// Mapped to `log.origin.file.line` field.
97    #[serde(skip_serializing_if = "Option::is_none")]
98    pub line: Option<u32>,
99
100    /// The filename of the source code which logged the message.
101    ///
102    /// Mapped to `log.origin.file.name` field.
103    #[serde(skip_serializing_if = "Option::is_none")]
104    pub name: Option<&'a str>,
105}
106
107/// Rust-specific information about the source code which logged the message.
108#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
109pub struct LogOriginRust<'a> {
110    /// The name of the log target.
111    ///
112    /// Mapped to `log.origin.rust.target` field.
113    pub target: &'a str,
114
115    /// The module path of the source code which logged the message.
116    ///
117    /// Mapped to `log.origin.rust.module_path` field.
118    #[serde(skip_serializing_if = "Option::is_none")]
119    pub module_path: Option<&'a str>,
120
121    /// The file path of the source code which logged the message.
122    ///
123    /// Mapped to `log.origin.rust.file_path` field.
124    #[serde(skip_serializing_if = "Option::is_none")]
125    pub file_path: Option<&'a str>,
126}
127
128impl<'a> Event<'a> {
129    /// Creates ECS log event from a [`log::Record`].
130    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}