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}