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}