ecs_logger/
lib.rs

1//! A logger compatible with [Elastic Common Schema (ECS) Logging](https://www.elastic.co/guide/en/ecs-logging/overview/current/intro.html).
2//!
3//! ## Features
4//!
5//! - Configurable via the `RUST_LOG` environment variable.
6//!   - Uses [env_logger] under the hood.
7//!   - **All logging is disabled except for the `error` level by default.**
8//! - Logs are written to stderr by default.
9//!
10//! ## Installation
11//!
12//! Add the following to your `Cargo.toml` file:
13//!
14//! ```toml
15//! [dependencies]
16//! log = "0.4"
17//! ecs-logger = "1"
18//! ```
19//!
20//! ## Example
21//!
22//! In the following examples we assume the binary is `./example`.
23//!
24//! ### Basic logging
25//!
26//! ```
27//! use log::{debug, error};
28//!
29//! ecs_logger::init();
30//!
31//! debug!(
32//!     "this is a debug {}, which is NOT printed by default",
33//!     "message"
34//! );
35//! error!("this is printed by default");
36//! ```
37//!
38//! ```bash
39//! $ ./example
40//! {"@timestamp":"2021-11-26T15:25:22.321002600Z","log.level":"ERROR","message":"this is printed by default","ecs.version":"1.12.1","log.origin":{"file":{"line":13,"name":"example.rs"},"rust":{"target":"example::tests","module_path":"example::tests","file_path":"tests/example.rs"}}}
41//! ```
42//!
43//! ```bash
44//! $ RUST_LOG=debug ./example
45//! {"@timestamp":"2021-11-26T15:26:13.524069Z","log.level":"DEBUG","message":"this is a debug message, which is NOT printed by default","ecs.version":"1.12.1","log.origin":{"file":{"line":9,"name":"example.rs"},"rust":{"target":"example::tests","module_path":"example::tests","file_path":"tests/example.rs"}}}
46//! {"@timestamp":"2021-11-26T15:26:13.524193100Z","log.level":"ERROR","message":"this is printed by default","ecs.version":"1.12.1","log.origin":{"file":{"line":13,"name":"example.rs"},"rust":{"target":"example::tests","module_path":"example::tests","file_path":"tests/example.rs"}}}
47//! ```
48//!
49//! More filtering config examples are available at [`env_logger`]'s documentation.
50//!
51//! ### Extra Fields
52//!
53//! You can add extra fields to the log output by using the [`extra_fields`] module.
54//!
55//! ```
56//! use ecs_logger::extra_fields;
57//! use serde::Serialize;
58//!
59//! #[derive(Serialize)]
60//! struct MyExtraFields {
61//!   my_field: String,
62//! }
63//!
64//! ecs_logger::init();
65//!
66//! extra_fields::set_extra_fields(MyExtraFields {
67//!   my_field: "my_value".to_string(),
68//! }).unwrap();
69//!
70//! log::error!("Hello {}!", "world");
71//! log::info!("Goodbye {}!", "world");
72//!
73//! extra_fields::clear_extra_fields();
74//! ```
75//!
76//! ### Custom logging
77//!
78//! You need to add [`env_logger`] to your `Cargo.toml` for the following examples.
79//!
80//! ```toml
81//! [dependencies]
82//! log = "0.4"
83//! env_logger = "0.9"
84//! ecs-logger = "1"
85//! ```
86//!
87//! #### Write to stdout
88//!
89//! ```
90//! use log::info;
91//!
92//! // Initialize custom logger
93//! env_logger::builder()
94//!     .format(ecs_logger::format) // Configure ECS logger
95//!     .target(env_logger::Target::Stdout) // Write to stdout
96//!     .init();
97//!
98//! info!("Hello {}!", "world");
99//! ```
100//!
101//! #### Configure log filters
102//!
103//! ```
104//! use log::info;
105//!
106//! // Initialize custom logger
107//! env_logger::builder()
108//!     .parse_filters("info,my_app=debug") // Set filters
109//!     .format(ecs_logger::format) // Configure ECS logger
110//!     .init();
111//!
112//! info!("Hello {}!", "world");
113//! ```
114//!
115//! ## Default log fields
116//!
117//! ```json
118//! {
119//!     "@timestamp": "2021-11-26T15:25:22.321002600Z",
120//!     "log.level": "ERROR",
121//!     "message": "this is printed by default",
122//!     "ecs.version": "1.12.1",
123//!     "log.origin": {
124//!         "file": {
125//!             "line": 13,
126//!             "name": "example.rs"
127//!         },
128//!         "rust": {
129//!             "target": "example::tests",
130//!             "module_path": "example::tests",
131//!             "file_path": "tests/example.rs"
132//!         }
133//!     }
134//! }
135//! ```
136
137pub mod ecs;
138pub mod extra_fields;
139mod timestamp;
140
141use ecs::Event;
142use extra_fields::merge_extra_fields;
143use std::borrow::BorrowMut;
144
145/// Initializes the global logger with an instance of [`env_logger::Logger`] with ECS-Logging formatting.
146///
147/// This should be called early in the execution of a Rust program. Any log events that occur before initialization will be ignored.
148///
149/// # Panics
150///
151/// This function will panic if it is called more than once, or if another library has already initialized a global logger.
152///
153/// # Example
154///
155/// ```
156/// use log::error;
157///
158/// error!("this is NOT logged");
159///
160/// ecs_logger::init();
161///
162/// error!("this is logged");
163/// ```
164pub fn init() {
165    try_init().expect("ecs_logger::init should not be called after logger initialized");
166}
167
168/// Attempts to initialize the global logger with an instance of [`env_logger::Logger`] with ECS-Logging formatting.
169///
170/// This should be called early in the execution of a Rust program. Any log events that occur before initialization will be ignored.
171///
172/// # Errors
173///
174/// This function returns [`log::SetLoggerError`] if it is called more than once, or if another library has already initialized a global logger.
175///
176/// # Example
177///
178/// ```
179/// use log::error;
180///
181/// error!("this is NOT logged");
182///
183/// assert!(ecs_logger::try_init().is_ok());
184///
185/// error!("this is logged");
186///
187/// // try_init should not be called more than once
188/// assert!(ecs_logger::try_init().is_err());
189/// ```
190pub fn try_init() -> Result<(), log::SetLoggerError> {
191    env_logger::builder().format(format).try_init()
192}
193
194/// Writes an ECS log line to the `buf`.
195///
196/// You may pass this format function to [`env_logger::Builder::format`] when building a custom logger.
197///
198/// # Example
199///
200/// ```
201/// use log::info;
202///
203/// // Initialize custom logger
204/// env_logger::builder()
205///     .parse_filters("info,my_app=debug") // Set filters
206///     .format(ecs_logger::format) // Configure ECS logger
207///     .target(env_logger::Target::Stdout) // Write to stdout
208///     .init();
209///
210/// info!("Hello {}!", "world");
211/// ```
212pub fn format(buf: &mut impl std::io::Write, record: &log::Record) -> std::io::Result<()> {
213    let event = Event::new(timestamp::get_timestamp(), record);
214
215    let event_json_value =
216        serde_json::to_value(event).expect("Event should be converted into JSON");
217    let event_json_map = match event_json_value {
218        serde_json::Value::Object(m) => m,
219        _ => unreachable!("Event should be converted into a JSON object"),
220    };
221
222    let merged_json_map = merge_extra_fields(event_json_map);
223
224    serde_json::to_writer(buf.borrow_mut(), &merged_json_map)?;
225    writeln!(buf)?;
226
227    Ok(())
228}
229
230#[cfg(test)]
231mod tests {
232    use super::*;
233    use serde_json::json;
234
235    #[test]
236    fn test_init() {
237        init();
238        assert!(try_init().is_err());
239    }
240
241    #[test]
242    fn test_format() {
243        extra_fields::clear_extra_fields();
244
245        let mut buf = Vec::new();
246        let record = create_example_record();
247        format(&mut buf, &record).unwrap();
248
249        let log_line = String::from_utf8(buf).unwrap();
250        assert_eq!(
251            log_line,
252            json!({
253                "@timestamp": timestamp::MOCK_TIMESTAMP,
254                "log.level": "ERROR",
255                "message": "hello world",
256                "ecs.version": "1.12.1",
257                "log.origin": {
258                    "file": {
259                        "line": 13,
260                        "name": "example.rs"
261                    },
262                    "rust": {
263                        "target": "example",
264                        "module_path": "example::tests",
265                        "file_path": "tests/example.rs"
266                    }
267                }
268            })
269            .to_string()
270                + "\n"
271        );
272    }
273
274    #[test]
275    fn test_format_with_extra_fields() {
276        extra_fields::set_extra_fields(json!({
277            "a": 1,
278            "b": {
279                "c": 2,
280            },
281        }))
282        .unwrap();
283
284        let mut buf = Vec::new();
285        let record = create_example_record();
286        format(&mut buf, &record).unwrap();
287
288        let log_line = String::from_utf8(buf).unwrap();
289        assert_eq!(
290            log_line,
291            json!({
292                "@timestamp": timestamp::MOCK_TIMESTAMP,
293                "log.level": "ERROR",
294                "message": "hello world",
295                "ecs.version": "1.12.1",
296                "log.origin": {
297                    "file": {
298                        "line": 13,
299                        "name": "example.rs"
300                    },
301                    "rust": {
302                        "target": "example",
303                        "module_path": "example::tests",
304                        "file_path": "tests/example.rs"
305                    }
306                },
307                "a": 1,
308                "b": {
309                    "c": 2,
310                },
311            })
312            .to_string()
313                + "\n"
314        );
315    }
316
317    fn create_example_record<'a>() -> log::Record<'a> {
318        log::Record::builder()
319            .args(format_args!("hello world"))
320            .level(log::Level::Error)
321            .target("example")
322            .file(Some("tests/example.rs"))
323            .line(Some(13))
324            .module_path(Some("example::tests"))
325            .build()
326    }
327}