log4rs/lib.rs
1//! log4rs is a highly configurable logging framework modeled after Java's
2//! Logback and log4j libraries.
3//!
4//! # Architecture
5//!
6//! The basic units of configuration are *appenders*, *encoders*, *filters*, and
7//! *loggers*.
8//!
9//! ## Appenders
10//!
11//! An appender takes a log record and logs it somewhere, for example, to a
12//! file, the console, or the syslog.
13//!
14//! Implementations:
15//! - [console](append/console/struct.ConsoleAppenderDeserializer.html#configuration): requires the `console_appender` feature.
16//! - [file](append/file/struct.FileAppenderDeserializer.html#configuration): requires the `file_appender` feature.
17//! - [rolling_file](append/rolling_file/struct.RollingFileAppenderDeserializer.html#configuration): requires the `rolling_file_appender` feature and can be configured with the `compound_policy`.
18//! - [compound](append/rolling_file/policy/compound/struct.CompoundPolicyDeserializer.html#configuration): requires the `compound_policy` feature
19//! - Rollers
20//! - [delete](append/rolling_file/policy/compound/roll/delete/struct.DeleteRollerDeserializer.html#configuration): requires the `delete_roller` feature
21//! - [fixed_window](append/rolling_file/policy/compound/roll/fixed_window/struct.FixedWindowRollerDeserializer.html#configuration): requires the `fixed_window_roller` feature
22//! - Triggers
23//! - [size](append/rolling_file/policy/compound/trigger/size/struct.SizeTriggerDeserializer.html#configuration): requires the `size_trigger` feature
24//! - [time](append/rolling_file/policy/compound/trigger/tine/struct.TimeTriggerDeserializer.html#configuration): requires the `time_trigger` feature
25//! - [onstartup](append/rolling_file/policy/compound/trigger/tine/struct.OnStartUpTriggerDeserializer.html#configuration): requires the `onstartup_trigger` feature
26//!
27//! ## Encoders
28//!
29//! An encoder is responsible for taking a log record, transforming it into the
30//! appropriate output format, and writing it out. An appender will normally
31//! use an encoder internally.
32//!
33//! Implementations:
34//! - [pattern](encode/pattern/struct.PatternEncoderDeserializer.html#configuration): requires the `pattern_encoder` feature
35//! - [json](encode/json/struct.JsonEncoderDeserializer.html#configuration): requires the `json_encoder` feature
36//!
37//! ## Filters
38//!
39//! Filters are associated with appenders and, like the name would suggest,
40//! filter log events coming into that appender.
41//!
42//! Implementations:
43//! - [threshold](filter/threshold/struct.ThresholdFilterDeserializer.html#configuration): requires the `threshold_filter` feature
44//!
45//! ## Loggers
46//!
47//! A log event is targeted at a specific logger, which are identified by
48//! string names. The logging macros built in to the `log` crate set the logger
49//! of a log event to the one identified by the module containing the
50//! invocation location.
51//!
52//! Loggers form a hierarchy: logger names are divided into components by "::".
53//! One logger is the ancestor of another if the first logger's component list
54//! is a prefix of the second logger's component list.
55//!
56//! Loggers are associated with a maximum log level. Log events for that logger
57//! with a level above the maximum will be ignored. The maximum log level for
58//! any logger can be configured manually; if it is not, the level will be
59//! inherited from the logger's parent.
60//!
61//! Loggers are also associated with a set of appenders. Appenders can be
62//! associated directly with a logger. In addition, the appenders of the
63//! logger's parent will be associated with the logger unless the logger has
64//! its *additive* set to `false`. Log events sent to the logger that are not
65//! filtered out by the logger's maximum log level will be sent to all
66//! associated appenders.
67//!
68//! The "root" logger is the ancestor of all other loggers. Since it has no
69//! ancestors, its additivity cannot be configured.
70//!
71//! # Configuration
72//!
73//! For a detailed breakdown on configuration, refer to the
74//! [config module](config/index.html#configuration).
75//!
76//! log4rs makes heavy use of Cargo features to enable consumers to pick the
77//! functionality they wish to use. File-based configuration requires the `file`
78//! feature, and each file format requires its own feature as well. In addition,
79//! each component has its own feature. For example, YAML support requires the
80//! `yaml_format` feature and the console appender requires the
81//! `console_appender` feature.
82//!
83//! By default, the `all_components`, `gzip`, `file`, and `yaml_format` features
84//! are enabled.
85//!
86//! As a convenience, the `all_components` feature activates all logger components.
87//!
88//! # Examples
89//!
90//! ## Configuration via a YAML file
91//!
92//! ```yaml
93//! # Scan this file for changes every 30 seconds
94//! refresh_rate: 30 seconds
95//!
96//! appenders:
97//! # An appender named "stdout" that writes to stdout
98//! stdout:
99//! kind: console
100//!
101//! # An appender named "requests" that writes to a file with a custom pattern encoder
102//! requests:
103//! kind: file
104//! path: "log/requests.log"
105//! encoder:
106//! pattern: "{d} - {m}{n}"
107//!
108//! # Set the default logging level to "warn" and attach the "stdout" appender to the root
109//! root:
110//! level: warn
111//! appenders:
112//! - stdout
113//!
114//! loggers:
115//! # Raise the maximum log level for events sent to the "app::backend::db" logger to "info"
116//! app::backend::db:
117//! level: info
118//!
119//! # Route log events sent to the "app::requests" logger to the "requests" appender,
120//! # and *not* the normal appenders installed at the root
121//! app::requests:
122//! level: info
123//! appenders:
124//! - requests
125//! additive: false
126//! ```
127//!
128//! Add the following in your application initialization.
129//!
130//! ```no_run
131//! # #[cfg(feature = "config_parsing")]
132//! # fn f() {
133//! log4rs::init_file("log4rs.yml", Default::default()).unwrap();
134//! # }
135//! ```
136//!
137//! ## Programmatically constructing a configuration:
138//!
139//! ```no_run
140//! # #[cfg(all(feature = "console_appender",
141//! # feature = "file_appender",
142//! # feature = "pattern_encoder"))]
143//! # fn f() {
144//! use log::LevelFilter;
145//! use log4rs::append::console::ConsoleAppender;
146//! use log4rs::append::file::FileAppender;
147//! use log4rs::encode::pattern::PatternEncoder;
148//! use log4rs::config::{Appender, Config, Logger, Root};
149//!
150//! fn main() {
151//! let stdout = ConsoleAppender::builder().build();
152//!
153//! let requests = FileAppender::builder()
154//! .encoder(Box::new(PatternEncoder::new("{d} - {m}{n}")))
155//! .build("log/requests.log")
156//! .unwrap();
157//!
158//! let config = Config::builder()
159//! .appender(Appender::builder().build("stdout", Box::new(stdout)))
160//! .appender(Appender::builder().build("requests", Box::new(requests)))
161//! .logger(Logger::builder().build("app::backend::db", LevelFilter::Info))
162//! .logger(Logger::builder()
163//! .appender("requests")
164//! .additive(false)
165//! .build("app::requests", LevelFilter::Info))
166//! .build(Root::builder().appender("stdout").build(LevelFilter::Warn))
167//! .unwrap();
168//!
169//! let handle = log4rs::init_config(config).unwrap();
170//!
171//! // use handle to change logger configuration at runtime
172//! }
173//! # }
174//! # fn main() {}
175//! ```
176//!
177//! ## Custom implementations of logging components
178//!
179//! You can impl some trait for your struct and use it with log4rs. For example:
180//! - Impl [log4rs::append::Append](append/trait.Append.html) for your custom appender.
181//! - Impl [log4rs::encode::Encode](encode/trait.Encode.html) for your custom encoder.
182//! - Impl [log4rs::filter::Filter](filter/trait.Filter.html) for your custom filter.
183//!
184//! Here is a very simple example to create a custom appender,
185//! for more examples about custom, see [examples/custom.rs](https://github.com/estk/log4rs/tree/main/examples/custom.rs):
186//! ```no_run
187//! # fn f() {
188//! use log4rs::append::Append;
189//! use log4rs::config::{Appender, Root};
190//!
191//! #[derive(Debug)]
192//! struct MyAppender(usize);
193//!
194//! // impl your process record logic here
195//! impl Append for MyAppender {
196//! fn append(&self, record: &log::Record) -> anyhow::Result<()> {
197//! println!("appender({}): {record:?}", self.0);
198//! Ok(())
199//! }
200//! fn flush(&self) {}
201//! }
202//!
203//! fn main() {
204//! let appender = MyAppender(100);
205//! let log_config = log4rs::config::Config::builder()
206//! .appender(Appender::builder().build("my_appender", Box::new(appender)))
207//! .build(
208//! Root::builder()
209//! .appender("my_appender")
210//! .build(log::LevelFilter::Info),
211//! )
212//! .unwrap();
213//! log4rs::init_config(log_config).unwrap();
214//! log::trace!("This is a trace message");
215//! log::info!("This is an info message");
216//! log::warn!("This is a warning message");
217//! }
218//! # }
219//! # fn main() {}
220//! ```
221//!
222//! To configure log4rs with a file, you should implement [log4rs::config::Deserialize](config/trait.Deserialize.html) for your config and be sure to register it with [log4rs::config::Deserializers](config/struct.Deserializers.html) as shown in the example below.
223//!
224//! Here is a very simple example to use a custom appender with custom config.
225//! For more examples about custom config file, see [examples/custom_config.rs](https://github.com/estk/log4rs/tree/main/examples/custom_config.rs):
226//! ```yaml
227//! # custom_config.yml
228//! appenders:
229//! my_appender:
230//! kind: custom_appender
231//! appender_data: 42
232//!
233//! root:
234//! level: INFO
235//! appenders:
236//! - my_appender
237//! ```
238//!
239//! ```no_run
240//! # #[cfg(feature = "config_parsing")]
241//! # fn f() {
242//! use log4rs::append::Append;
243//! use log4rs::config::{Deserialize, Deserializers};
244//!
245//! #[derive(Debug)]
246//! struct MyAppender(usize);
247//!
248//! // impl your process record logic here
249//! impl Append for MyAppender {
250//! fn append(&self, record: &log::Record) -> anyhow::Result<()> {
251//! println!("appender({}): {record:?}", self.0);
252//! Ok(())
253//! }
254//! fn flush(&self) {}
255//! }
256//! // Define config struct for custom appender
257//! #[derive(serde::Deserialize)]
258//! pub struct MyAppenderConfig {
259//! pub appender_data: Option<usize>,
260//! }
261//!
262//! #[derive(Default)]
263//! pub struct MyAppenderDeserializer;
264//!
265//! // impl Deserialize for custom appender config
266//! impl Deserialize for MyAppenderDeserializer {
267//! type Trait = dyn Append;
268//!
269//! type Config = MyAppenderConfig;
270//!
271//! fn deserialize(
272//! &self,
273//! config: MyAppenderConfig,
274//! _: &Deserializers,
275//! ) -> anyhow::Result<Box<Self::Trait>> {
276//! let appender_data = config.appender_data.unwrap_or(0);
277//! let appender = MyAppender(appender_data);
278//! Ok(Box::new(appender))
279//! }
280//! }
281//!
282//! fn main() {
283//! let log_file = "custom_config.yml";
284//! // Access the default deserializers map
285//! let mut deserializers = Deserializers::default();
286//! // Register the "custom_appender" deserializer into the default deserializers map
287//! deserializers.insert("custom_appender", MyAppenderDeserializer);
288//! // Initialize log4rs with the custom deserializers from the file
289//! log4rs::init_file(log_file, deserializers).unwrap();
290//!
291//! log::trace!("This is a trace message");
292//! log::info!("This is an info message");
293//! log::warn!("This is a warning message");
294//! }
295//! # }
296//! # fn main() {}
297//! ```
298//!
299//! For more examples see the [examples](https://github.com/estk/log4rs/tree/main/examples).
300//!
301
302#![allow(clippy::manual_non_exhaustive)]
303#![warn(missing_docs)]
304
305use std::{
306 cmp, collections::HashMap, fmt, hash::BuildHasherDefault, io, io::prelude::*, sync::Arc,
307};
308
309use arc_swap::ArcSwap;
310use fnv::FnvHasher;
311use log::{Level, LevelFilter, Metadata, Record};
312
313pub mod append;
314pub mod config;
315pub mod encode;
316pub mod filter;
317#[cfg(feature = "console_writer")]
318mod priv_io;
319
320pub use config::{init_config, Config};
321
322#[cfg(feature = "config_parsing")]
323pub use config::{init_file, init_raw_config};
324
325use self::{append::Append, filter::Filter};
326
327type FnvHashMap<K, V> = HashMap<K, V, BuildHasherDefault<FnvHasher>>;
328
329#[derive(Debug)]
330struct ConfiguredLogger {
331 level: LevelFilter,
332 appenders: Vec<usize>,
333 children: FnvHashMap<String, ConfiguredLogger>,
334}
335
336impl ConfiguredLogger {
337 fn add(&mut self, path: &str, mut appenders: Vec<usize>, additive: bool, level: LevelFilter) {
338 let (part, rest) = match path.find("::") {
339 Some(idx) => (&path[..idx], &path[idx + 2..]),
340 None => (path, ""),
341 };
342
343 if let Some(child) = self.children.get_mut(part) {
344 child.add(rest, appenders, additive, level);
345 return;
346 }
347
348 let child = if rest.is_empty() {
349 if additive {
350 appenders.extend(self.appenders.iter().cloned());
351 }
352
353 ConfiguredLogger {
354 level,
355 appenders,
356 children: FnvHashMap::default(),
357 }
358 } else {
359 let mut child = ConfiguredLogger {
360 level: self.level,
361 appenders: self.appenders.clone(),
362 children: FnvHashMap::default(),
363 };
364 child.add(rest, appenders, additive, level);
365 child
366 };
367
368 self.children.insert(part.to_owned(), child);
369 }
370
371 fn max_log_level(&self) -> LevelFilter {
372 let mut max = self.level;
373 for child in self.children.values() {
374 max = cmp::max(max, child.max_log_level());
375 }
376 max
377 }
378
379 fn find(&self, path: &str) -> &ConfiguredLogger {
380 let mut node = self;
381
382 for part in path.split("::") {
383 match node.children.get(part) {
384 Some(child) => node = child,
385 None => break,
386 }
387 }
388
389 node
390 }
391
392 fn enabled(&self, level: Level) -> bool {
393 self.level >= level
394 }
395
396 fn log(&self, record: &log::Record, appenders: &[Appender]) -> Result<(), Vec<anyhow::Error>> {
397 let mut errors = vec![];
398 if self.enabled(record.level()) {
399 for &idx in &self.appenders {
400 if let Err(err) = appenders[idx].append(record) {
401 errors.push(err);
402 }
403 }
404 }
405
406 if errors.is_empty() {
407 Ok(())
408 } else {
409 Err(errors)
410 }
411 }
412}
413
414#[derive(Debug)]
415struct Appender {
416 appender: Box<dyn Append>,
417 filters: Vec<Box<dyn Filter>>,
418}
419
420impl Appender {
421 fn append(&self, record: &Record) -> anyhow::Result<()> {
422 for filter in &self.filters {
423 match filter.filter(record) {
424 filter::Response::Accept => break,
425 filter::Response::Neutral => {}
426 filter::Response::Reject => return Ok(()),
427 }
428 }
429
430 self.appender.append(record)
431 }
432
433 fn flush(&self) {
434 self.appender.flush();
435 }
436}
437
438struct SharedLogger {
439 root: ConfiguredLogger,
440 appenders: Vec<Appender>,
441 err_handler: Box<dyn Send + Sync + Fn(&anyhow::Error)>,
442}
443
444impl fmt::Debug for SharedLogger {
445 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
446 f.debug_struct("SharedLogger")
447 .field("root", &self.root)
448 .field("appenders", &self.appenders)
449 .finish()
450 }
451}
452
453impl SharedLogger {
454 fn new(config: config::Config) -> SharedLogger {
455 Self::new_with_err_handler(
456 config,
457 Box::new(|e: &anyhow::Error| {
458 let _ = writeln!(io::stderr(), "log4rs: {}", e);
459 }),
460 )
461 }
462 fn new_with_err_handler(
463 config: config::Config,
464 err_handler: Box<dyn Send + Sync + Fn(&anyhow::Error)>,
465 ) -> SharedLogger {
466 let (appenders, root, mut loggers) = config.unpack();
467
468 let root = {
469 let appender_map = appenders
470 .iter()
471 .enumerate()
472 .map(|(i, appender)| (appender.name(), i))
473 .collect::<HashMap<_, _>>();
474
475 let mut root = ConfiguredLogger {
476 level: root.level(),
477 appenders: root
478 .appenders()
479 .iter()
480 .map(|appender| appender_map[&**appender])
481 .collect(),
482 children: FnvHashMap::default(),
483 };
484
485 // sort loggers by name length to ensure that we initialize them top to bottom
486 loggers.sort_by_key(|l| l.name().len());
487 for logger in loggers {
488 let appenders = logger
489 .appenders()
490 .iter()
491 .map(|appender| appender_map[&**appender])
492 .collect();
493 root.add(logger.name(), appenders, logger.additive(), logger.level());
494 }
495
496 root
497 };
498
499 let appenders = appenders
500 .into_iter()
501 .map(|appender| {
502 let (_, appender, filters) = appender.unpack();
503 Appender { appender, filters }
504 })
505 .collect();
506
507 SharedLogger {
508 root,
509 appenders,
510 err_handler,
511 }
512 }
513}
514
515/// The fully configured log4rs Logger which is appropriate
516/// to use with the `log::set_boxed_logger` function.
517#[derive(Debug)]
518pub struct Logger(Arc<ArcSwap<SharedLogger>>);
519
520impl Logger {
521 /// Create a new `Logger` given a configuration.
522 pub fn new(config: config::Config) -> Logger {
523 Logger(Arc::new(ArcSwap::new(Arc::new(SharedLogger::new(config)))))
524 }
525 /// Create a new `Logger` given a configuration and err handler.
526 pub fn new_with_err_handler(
527 config: config::Config,
528 err_handler: Box<dyn Send + Sync + Fn(&anyhow::Error)>,
529 ) -> Logger {
530 Logger(Arc::new(ArcSwap::new(Arc::new(
531 SharedLogger::new_with_err_handler(config, err_handler),
532 ))))
533 }
534
535 /// Set the max log level above which everything will be filtered.
536 pub fn max_log_level(&self) -> LevelFilter {
537 self.0.load().root.max_log_level()
538 }
539 /// Get a `Handler` instance to reconfigure logger while running
540 pub fn handle(&self) -> Handle {
541 Handle {
542 shared: self.0.clone(),
543 }
544 }
545}
546
547impl log::Log for Logger {
548 fn enabled(&self, metadata: &Metadata) -> bool {
549 self.0
550 .load()
551 .root
552 .find(metadata.target())
553 .enabled(metadata.level())
554 }
555
556 fn log(&self, record: &log::Record) {
557 let shared = self.0.load();
558 if let Err(errs) = shared
559 .root
560 .find(record.target())
561 .log(record, &shared.appenders)
562 {
563 for e in errs {
564 (shared.err_handler)(&e)
565 }
566 }
567 }
568
569 fn flush(&self) {
570 for appender in &self.0.load().appenders {
571 appender.flush();
572 }
573 }
574}
575
576pub(crate) fn handle_error(e: &anyhow::Error) {
577 let _ = writeln!(io::stderr(), "log4rs: {}", e);
578}
579
580/// A handle to the active logger.
581#[derive(Clone, Debug)]
582pub struct Handle {
583 shared: Arc<ArcSwap<SharedLogger>>,
584}
585
586impl Handle {
587 /// Sets the logging configuration.
588 pub fn set_config(&self, config: Config) {
589 let shared = SharedLogger::new(config);
590 log::set_max_level(shared.root.max_log_level());
591 self.shared.store(Arc::new(shared));
592 }
593
594 /// Get the maximum log level according to the current configuration
595 pub fn max_log_level(&self) -> LevelFilter {
596 self.shared.load().root.max_log_level()
597 }
598}
599
600#[cfg(test)]
601mod test {
602 use log::{Level, LevelFilter, Log};
603
604 use super::*;
605
606 #[test]
607 #[cfg(all(feature = "config_parsing", feature = "json_format"))]
608 fn init_from_raw_config() {
609 let dir = tempfile::tempdir().unwrap();
610 let path = dir.path().join("append.log");
611
612 let cfg = serde_json::json!({
613 "refresh_rate": "60 seconds",
614 "root" : {
615 "appenders": ["baz"],
616 "level": "info",
617 },
618 "appenders": {
619 "baz": {
620 "kind": "file",
621 "path": path,
622 "encoder": {
623 "pattern": "{m}"
624 }
625 }
626 },
627 });
628 let config = serde_json::from_str::<config::RawConfig>(&cfg.to_string()).unwrap();
629 if let Err(e) = init_raw_config(config) {
630 panic!("{}", e);
631 }
632 assert!(path.exists());
633 log::info!("init_from_raw_config");
634
635 let mut contents = String::new();
636 std::fs::File::open(&path)
637 .unwrap()
638 .read_to_string(&mut contents)
639 .unwrap();
640 assert_eq!(contents, "init_from_raw_config");
641 }
642
643 #[test]
644 fn enabled() {
645 let root = config::Root::builder().build(LevelFilter::Debug);
646 let mut config = config::Config::builder();
647 let logger = config::Logger::builder().build("foo::bar", LevelFilter::Trace);
648 config = config.logger(logger);
649 let logger = config::Logger::builder().build("foo::bar::baz", LevelFilter::Off);
650 config = config.logger(logger);
651 let logger = config::Logger::builder().build("foo::baz::buz", LevelFilter::Error);
652 config = config.logger(logger);
653 let config = config.build(root).unwrap();
654
655 let logger = super::Logger::new(config);
656
657 assert!(logger.enabled(&Metadata::builder().level(Level::Warn).target("bar").build()));
658 assert!(!logger.enabled(
659 &Metadata::builder()
660 .level(Level::Trace)
661 .target("bar")
662 .build()
663 ));
664 assert!(logger.enabled(
665 &Metadata::builder()
666 .level(Level::Debug)
667 .target("foo")
668 .build()
669 ));
670 assert!(logger.enabled(
671 &Metadata::builder()
672 .level(Level::Trace)
673 .target("foo::bar")
674 .build()
675 ));
676 assert!(!logger.enabled(
677 &Metadata::builder()
678 .level(Level::Error)
679 .target("foo::bar::baz")
680 .build()
681 ));
682 assert!(logger.enabled(
683 &Metadata::builder()
684 .level(Level::Debug)
685 .target("foo::bar::bazbuz")
686 .build()
687 ));
688 assert!(!logger.enabled(
689 &Metadata::builder()
690 .level(Level::Error)
691 .target("foo::bar::baz::buz")
692 .build()
693 ));
694 assert!(!logger.enabled(
695 &Metadata::builder()
696 .level(Level::Warn)
697 .target("foo::baz::buz")
698 .build()
699 ));
700 assert!(!logger.enabled(
701 &Metadata::builder()
702 .level(Level::Warn)
703 .target("foo::baz::buz::bar")
704 .build()
705 ));
706 assert!(logger.enabled(
707 &Metadata::builder()
708 .level(Level::Error)
709 .target("foo::baz::buz::bar")
710 .build()
711 ));
712 }
713}