1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927
#![doc(test(attr(deny(warnings))))] #![forbid(unsafe_code)] #![warn(missing_docs)] #![allow( unknown_lints, clippy::unknown_clippy_lints, clippy::needless_doctest_main )] //! A [`spirit`] fragments and helpers to configure and control logging. //! //! The [`Fragment`]s here allow to configure relatively complex logging (multiple loggers, //! different formats, different destinations), both from command line and the configuration. It //! allows runtime reloading of them. //! //! Internally it is based on the [`fern`] crate and just adds the configuration and runtime //! reloading (through [`log-reroute`]). //! //! It assumes the application doesn't set the global logger itself. It also sets the panic hook //! through the [`log_panics`] crate. The `with-backtrace` cargo feature is propagated through. //! //! # Features //! //! * `background`: Includes the ability to log asynchronously ‒ the writing to log files happens //! in a background thread and allows the rest of the application to not block on IO. //! * `cfg-help`: Support for configuration options help at runtime. On by default. //! * `with-backtrace`: The [`log_panics`] logs with backtraces. On by default. //! * `syslog`: Adds the support for logging into syslog. //! //! # Startup //! //! When using the automatic management with a pipeline, this is how a startup happens: //! //! * As soon as the pipeline is registered, a logging on the `WARN` level is sent to `stderr`. //! * After command line arguments are parsed the `stderr` logging is updated to reflect that (or //! left on the `WARN` level if nothing is set by the user). //! * After configuration is loaded from the files, full logging is configured according to that. //! //! # Integration with other loggers //! //! If you need something specific (for example [`sentry`](https://crates.io/crates/sentry)), you //! can plug in additional loggers through the pipeline ‒ the [`Dispatch`] allows adding arbitrary //! loggers. The [`Pipeline::map`][spirit::fragment::pipeline::Pipeline::map] is a good place to do //! it. //! //! # Performance warning //! //! This allows the user to create arbitrary number of loggers. Furthermore, the logging is //! synchronous by default and not buffered. When writing a lot of logs or sending them over the //! network, this could become a bottleneck. //! //! # Background logging //! //! The `background` feature flag adds the ability to do the actual logging in a background thread. //! This allows not blocking the actual application by IO or other expensive operations. //! //! On the other hand, if the application crashes, some logs may be lost (or, depending on setup, //! when the logging thread doesn't keep up). Also, you need to flush the logger on shutdown, by //! using the [`FlushGuard`]. //! //! It is done through the [`Background`] transformation. //! //! # Planned features //! //! These pieces are planned some time in future, but haven't happened yet (pull requests are //! welcome). //! //! * Reconnecting to the remote server if a TCP connection is lost. //! * Log file rotation. //! * Colors on `stdout`/`stderr`. //! //! # Usage without Pipelines //! //! It is possible to use without the [`Pipeline`][spirit::Pipeline], manually. However, //! certain care needs to be taken to initialize everything that needs to be initialized. //! //! It is either possible to just get the [`Dispatch`] object and call [`apply`][Dispatch::apply], //! that however is a single-shot initialization and the logger can't be replaced. //! //! The helper functions [`init`] and [`install`] can be used to gain the ability to replace //! [`Dispatch`] loggers multiple times. //! //! # Examples //! //! ## Manual single use installation //! //! ```rust //! use spirit::AnyError; //! use spirit::prelude::*; //! use spirit_log::Cfg; //! //! # fn main() -> Result<(), AnyError> { //! // Well, you'd get it somewhere from configuration, but… //! let cfg = Cfg::default(); //! let logger = cfg.create("logger")?; //! logger.apply()?; //! # Ok(()) } //! ``` //! //! ## Manual multiple-use installation //! //! ```rust //! use spirit::AnyError; //! use spirit::prelude::*; //! use spirit_log::Cfg; //! //! # fn main() -> Result<(), AnyError> { //! spirit_log::init(); //! // This part can be done multiple times. //! let cfg = Cfg::default(); //! let logger = cfg.create("logger")?; //! spirit_log::install(logger); //! # Ok(()) } //! ``` //! //! ## Automatic usage with a Pipeline, reloading and command line options //! //! ```rust //! use log::info; //! use serde::Deserialize; //! use spirit::{Pipeline, Spirit}; //! use spirit::prelude::*; //! use spirit_log::{Cfg as LogCfg, CfgAndOpts as LogBoth, Opts as LogOpts}; //! use structopt::StructOpt; //! //! #[derive(Clone, Debug, StructOpt)] //! struct Opts { //! #[structopt(flatten)] //! logging: LogOpts, //! } //! //! impl Opts { //! fn logging(&self) -> LogOpts { //! self.logging.clone() //! } //! } //! //! #[derive(Clone, Debug, Default, Deserialize)] //! struct Cfg { //! #[serde(default, skip_serializing_if = "LogCfg::is_empty")] //! logging: LogCfg, //! } //! //! impl Cfg { //! fn logging(&self) -> LogCfg { //! self.logging.clone() //! } //! } //! //! fn main() { //! Spirit::<Opts, Cfg>::new() //! .with( //! Pipeline::new("logging").extract(|opts: &Opts, cfg: &Cfg| LogBoth { //! cfg: cfg.logging(), //! opts: opts.logging(), //! }), //! ) //! .run(|_spirit| { //! info!("Hello world"); //! Ok(()) //! }); //! } //! ``` //! //! The configuration could look something like this: //! //! ```toml //! [[logging]] //! level = "DEBUG" //! type = "file" //! filename = "/tmp/example.log" //! clock = "UTC" //! ``` //! //! [`log-reroute`]: https://docs.rs/log-reroute //! [`log_panics`]: https://docs.rs/log_panics use std::cmp; use std::collections::HashMap; use std::fmt::Arguments; use std::io::{self, Write}; use std::iter; use std::net::TcpStream; use std::path::PathBuf; use std::sync::atomic::{AtomicBool, Ordering}; use std::thread; use chrono::format::{DelayedFormat, StrftimeItems}; use chrono::{Local, Utc}; use fern::Dispatch; use itertools::Itertools; use log::{debug, trace, LevelFilter, Log, STATIC_MAX_LEVEL}; use serde::de::{Deserializer, Error as DeError}; use serde::ser::Serializer; use serde::{Deserialize, Serialize}; use spirit::extension::{Extensible, Extension}; use spirit::fragment::driver::Trivial as TrivialDriver; use spirit::fragment::{Fragment, Installer}; use spirit::AnyError; #[cfg(feature = "cfg-help")] use structdoc::StructDoc; use structopt::StructOpt; #[cfg(feature = "background")] pub mod background; #[cfg(feature = "background")] pub use background::{Background, FlushGuard, OverflowMode}; const UNKNOWN_THREAD: &str = "<unknown>"; // Workaround for https://github.com/TeXitoi/structopt/issues/333 #[cfg_attr(not(doc), allow(missing_docs))] #[cfg_attr( doc, doc = r#" A fragment for command line options. By flattening this into the top-level `StructOpt` structure, you get the `-l` and `-L` command line options. The `-l` (`--log`) sets the global logging level for `stderr`. The `-L` accepts pairs (eg. `-L spirit=TRACE`) specifying levels for specific logging targets. If used, the logging will be sent to `stderr`. "# )] #[derive(Clone, Debug, StructOpt)] pub struct Opts { /// Log to stderr with this log level. #[structopt(short = "l", long = "log", number_of_values(1))] log: Option<LevelFilter>, /// Log to stderr with overriden levels for specific modules. #[structopt( short = "L", long = "log-module", parse(try_from_str = spirit::utils::key_val), number_of_values(1), )] log_modules: Vec<(String, LevelFilter)>, } impl Opts { fn logger_cfg(&self) -> Option<Logger> { self.log.map(|level| Logger { level: LevelFilterSerde(level), destination: LogDestination::StdErr, per_module: self .log_modules .iter() .map(|(module, lf)| (module.clone(), LevelFilterSerde(*lf))) .collect(), clock: Clock::Local, time_format: cmdline_time_format(), format: Format::Short, }) } } // TODO: OptsExt & OptsVerbose and turn the other things into Into<Opts> #[derive(Clone, Debug, Deserialize, Serialize, PartialEq)] #[cfg_attr(feature = "cfg-help", derive(StructDoc))] #[serde(tag = "type", rename_all = "kebab-case")] // TODO: Make deny-unknown-fields work enum LogDestination { /// Writes the logs into a file. File { /// The path to the file to store the log into. /// /// The file will be appended to or created if it doesn't exist. The directory it resides /// in must already exist. /// /// There is no direct support for log rotation. However, as the log file is reopened on /// `SIGHUP`, the usual external logrotate setup should work. filename: PathBuf, // TODO: Truncate }, /// Sends the logs to local syslog. /// /// Note that syslog ignores formatting options. #[cfg(feature = "syslog")] Syslog { /// Overrides the host value in the log messages. #[serde(skip_serializing_if = "Option::is_none")] host: Option<String>, // TODO: Remote syslog }, /// Sends the logs over a TCP connection over the network. Network { /// Hostname or IP address of the remote machine. host: String, /// Port to connect to on the remote machine. port: u16, }, /// Writes logs to standard output. #[serde(rename = "stdout")] StdOut, // TODO: Colors /// Writes the logs to error output. #[serde(rename = "stderr")] StdErr, // TODO: Colors } const LEVEL_FILTERS: &[&str] = &["OFF", "ERROR", "WARN", "INFO", "DEBUG", "TRACE"]; // A newtype to help us with serde, structdoc, default... more convenient inside maps and such. // // We could get serde support with a feature flag from log itself, but not the rest :-(. #[derive(Copy, Clone, Debug)] struct LevelFilterSerde(LevelFilter); impl Default for LevelFilterSerde { fn default() -> LevelFilterSerde { LevelFilterSerde(LevelFilter::Error) } } impl<'de> Deserialize<'de> for LevelFilterSerde { fn deserialize<D: Deserializer<'de>>(d: D) -> Result<LevelFilterSerde, D::Error> { let s = String::deserialize(d)?; s.parse() .map(LevelFilterSerde) .map_err(|_| D::Error::unknown_variant(&s, LEVEL_FILTERS)) } } impl Serialize for LevelFilterSerde { fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> { s.serialize_str(&format!("{:?}", self.0).to_uppercase()) } } #[cfg(feature = "cfg-help")] impl structdoc::StructDoc for LevelFilterSerde { fn document() -> structdoc::Documentation { use structdoc::{Documentation, Field, Tagging}; let filters = LEVEL_FILTERS .iter() .map(|name| (*name, Field::new(Documentation::leaf_empty(), ""))); Documentation::enum_(filters, Tagging::External) } } /// This error can be returned when initialization of logging to syslog fails. #[derive(Clone, Debug)] #[cfg(feature = "syslog")] pub struct SyslogError(String); #[cfg(feature = "syslog")] impl std::fmt::Display for SyslogError { fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result { self.0.fmt(fmt) } } #[cfg(feature = "syslog")] impl std::error::Error for SyslogError {} #[derive(Clone, Copy, Debug, Deserialize, Serialize)] #[cfg_attr(feature = "cfg-help", derive(StructDoc))] #[serde(rename_all = "SCREAMING_SNAKE_CASE")] enum Clock { Local, Utc, } impl Clock { fn now(self, format: &str) -> DelayedFormat<StrftimeItems> { match self { Clock::Local => Local::now().format(format), Clock::Utc => Utc::now().format(format), } } } impl Default for Clock { fn default() -> Self { Clock::Local } } fn default_time_format() -> String { "%+".to_owned() } fn cmdline_time_format() -> String { "%F %T%.3f".to_owned() } #[derive(Clone, Copy, Debug, Deserialize, Serialize)] #[cfg_attr(feature = "cfg-help", derive(StructDoc))] #[serde(rename_all = "kebab-case")] enum Format { /// Only the message, without any other fields. MessageOnly, /// The time, log level, log target and message in columns. Short, /// The time, log level, thread name, log target and message in columns. Extended, /// The time, log level, thread name, file name and line, log target and message in columns. Full, /// The time, log level, thread name, file name and line, log target and message in columns /// separated by tabs. /// /// This format is simpler to machine-parse (because the columns are separated by a single '\t' /// character and only the last one should ever contain it), but less human-readable because /// the columns don't have to visually align. Machine, /// The time, log level, thread name, file name and line, log target and message, formatted as /// json with these field names: /// /// * timestamp /// * level /// * thread_name /// * file /// * line /// * target /// * message /// /// Each message is on a separate line and the JSONs are not pretty-printed (therefore it is /// one JSON per line). // TODO: Configurable field names? Json, /// Similar to `json`, however with field names that correspond to default configuration of /// logstash. /// /// * @timestamp /// * @version (always set to 1) /// * level /// * thread_name /// * logger_name (corresponds to log target) /// * message Logstash, // TODO: Custom } impl Default for Format { fn default() -> Self { Format::Short } } #[cfg(not(feature = "background"))] fn get_thread_name(thread: &thread::Thread) -> &str { thread.name().unwrap_or(UNKNOWN_THREAD) } #[cfg(feature = "background")] use background::get_thread_name; #[derive(Clone, Debug, Deserialize, Serialize)] #[cfg_attr(feature = "cfg-help", derive(StructDoc))] #[serde(rename_all = "kebab-case")] // TODO: Make deny-unknown-fields work struct Logger { #[serde(flatten)] destination: LogDestination, #[serde(default)] clock: Clock, /// The format of timestamp. /// /// This is strftime-like time format string, fully specified here: /// /// https://docs.rs/chrono/~0.4/chrono/format/strftime/index.html /// /// The default is %+, which corresponds to ISO 8601 / RFC 3339 date & time format. #[serde(default = "default_time_format")] time_format: String, /// Format of log messages. #[serde(default)] format: Format, /// The level on which to log messages. /// /// Messages with this level or more severe will be written into this logger. #[serde(default)] level: LevelFilterSerde, /// Overrides of log level per each module. /// /// The map allows for overriding log levels of each separate module (log target) separately. /// This allows silencing a verbose one or getting more info out of misbehaving one. #[serde(default, skip_serializing_if = "HashMap::is_empty")] per_module: HashMap<String, LevelFilterSerde>, } impl Logger { fn create(&self) -> Result<Dispatch, AnyError> { trace!("Creating logger for {:?}", self); let mut logger = Dispatch::new().level(self.level.0); logger = self .per_module .iter() .fold(logger, |logger, (module, level)| { logger.level_for(module.clone(), level.0) }); let clock = self.clock; let time_format = self.time_format.clone(); let format = self.format; // Clippy gets angry when the syslog is disabled #[allow(clippy::unknown_clippy_lints, clippy::match_single_binding)] match self.destination { // We don't want to format syslog #[cfg(feature = "syslog")] LogDestination::Syslog { .. } => (), // We do with the other things _ => { logger = logger.format(move |out, message, record| { match format { Format::MessageOnly => out.finish(format_args!("{}", message)), Format::Short => out.finish(format_args!( "{} {:5} {:30} {}", clock.now(&time_format), record.level(), record.target(), message, )), Format::Extended => { out.finish(format_args!( "{} {:5} {:30} {:30} {}", clock.now(&time_format), record.level(), get_thread_name(&thread::current()), record.target(), message, )); } Format::Full => { out.finish(format_args!( "{} {:5} {:10} {:>25}:{:<5} {:30} {}", clock.now(&time_format), record.level(), get_thread_name(&thread::current()), record.file().unwrap_or("<unknown>"), record.line().unwrap_or(0), record.target(), message, )); } Format::Machine => { out.finish(format_args!( "{}\t{}\t{}\t{}\t{}\t{}\t{}", clock.now(&time_format), record.level(), get_thread_name(&thread::current()), record.file().unwrap_or("<unknown>"), record.line().unwrap_or(0), record.target(), message, )); } Format::Json => { // We serialize it by putting things into a structure and using serde // for that. // // This is a zero-copy structure. #[derive(Serialize)] struct Msg<'a> { timestamp: Arguments<'a>, level: Arguments<'a>, thread_name: &'a str, file: Option<&'a str>, line: Option<u32>, target: &'a str, message: &'a Arguments<'a>, } // Unfortunately, the Arguments thing produced by format_args! doesn't // like to live in a variable ‒ all attempts to put it into a let // binding failed with various borrow-checker errors. // // However, constructing it as a temporary when calling a function // seems to work fine. So we use this closure to work around the // problem. let log = |msg: &Msg| { // TODO: Maybe use some shortstring or so here to avoid allocation? let msg = serde_json::to_string(msg) .expect("Failed to serialize JSON log"); out.finish(format_args!("{}", msg)); }; log(&Msg { timestamp: format_args!("{}", clock.now(&time_format)), level: format_args!("{}", record.level()), thread_name: &get_thread_name(&thread::current()), file: record.file(), line: record.line(), target: record.target(), message, }); } Format::Logstash => { // We serialize it by putting things into a structure and using serde // for that. // // This is a zero-copy structure. #[derive(Serialize)] struct Msg<'a> { #[serde(rename = "@timestamp")] timestamp: Arguments<'a>, #[serde(rename = "@version")] version: u8, level: Arguments<'a>, thread_name: &'a str, logger_name: &'a str, message: &'a Arguments<'a>, } // Unfortunately, the Arguments thing produced by format_args! doesn't // like to live in a variable ‒ all attempts to put it into a let // binding failed with various borrow-checker errors. // // However, constructing it as a temporary when calling a function // seems to work fine. So we use this closure to work around the // problem. let log = |msg: &Msg| { // TODO: Maybe use some shortstring or so here to avoid allocation? let msg = serde_json::to_string(msg) .expect("Failed to serialize JSON log"); out.finish(format_args!("{}", msg)); }; log(&Msg { timestamp: format_args!("{}", clock.now(&time_format)), version: 1, level: format_args!("{}", record.level()), thread_name: &get_thread_name(&thread::current()), logger_name: record.target(), message, }); } } }); } } match self.destination { LogDestination::File { ref filename } => Ok(logger.chain(fern::log_file(filename)?)), #[cfg(feature = "syslog")] LogDestination::Syslog { ref host } => { let formatter = syslog::Formatter3164 { facility: syslog::Facility::LOG_USER, hostname: host.clone(), // TODO: Does this give us the end-user crate or us? process: env!("CARGO_PKG_NAME").to_owned(), pid: 0, }; let sys_logger = syslog::unix(formatter).map_err(|e| SyslogError(format!("{}", e)))?; let sys_logger: Box<dyn Log> = Box::new(syslog::BasicLogger::new(sys_logger)); // TODO: Other destinations than just unix Ok(logger.chain(sys_logger)) } LogDestination::Network { ref host, port } => { // TODO: Reconnection support let conn = TcpStream::connect((&host as &str, port))?; Ok(logger.chain(Box::new(conn) as Box<dyn Write + Send>)) } LogDestination::StdOut => Ok(logger.chain(io::stdout())), LogDestination::StdErr => Ok(logger.chain(io::stderr())), } } } impl Default for Logger { fn default() -> Self { Self { destination: LogDestination::StdErr, level: LevelFilterSerde(LevelFilter::Warn), per_module: HashMap::new(), clock: Clock::Local, time_format: cmdline_time_format(), format: Format::Short, } } } fn create<'a, I>(logging: I) -> Result<Dispatch, AnyError> where I: IntoIterator<Item = &'a Logger>, { debug!("Creating loggers"); logging .into_iter() .map(Logger::create) .fold_results(Dispatch::new(), Dispatch::chain) .map_err(AnyError::from) } /// A configuration fragment to set up logging. /// /// By flattening this into the configuration structure, the program can load options for /// configuring logging. It adds a new top-level array `logging`. Each item describes one logger, /// with separate log levels and destination. /// /// See the [crate examples](index.html#examples) for the use. /// /// # Logger options /// /// These are valid for all loggers: /// /// * `level`: The log level to use. Valid options are `OFF`, `ERROR`, `WARN`, `INFO`, `DEBUG` and /// `TRACE`. /// * `per-module`: A map, setting log level overrides for specific modules (logging targets). This /// one is optional. /// * `type`: Specifies the type of logger destination. Some of them allow specifying other /// options. /// * `clock`: Either `LOCAL` or `UTC`. Defaults to `LOCAL` if not present. /// * `time_format`: Time /// [format string](https://docs.rs/chrono/*/chrono/format/strftime/index.html). Defaults to /// `%+` (which is ISO 8601/RFC 3339). Note that the command line logger (one produced by `-l`) /// uses a more human-friendly format. /// * `format`: The format to use. There are few presets (and a custom may come in future). /// - `message-only`: The line contains only the message itself. /// - `short`: This is the default. `<timestamp> <level> <target> <message>`. Padded to form /// columns. /// - `extended`: <timestamp> <level> <thread-name> <target> <message>`. Padded to form columns. /// - `full`: `<timestamp> <level> <thread-name> <file>:<line> <target> <message>`. Padded to /// form columns. /// - `machine`: Like `full`, but columns are not padded by spaces, they are separated by a /// single `\t` character, for more convenient processing by tools like `cut`. /// - `json`: The fields of `full` are encoded into a `json` format, for convenient processing of /// more modern tools like logstash. /// - `logstash`: `json` format with fields named and formatted according to /// [Logback JSON encoder](https://github.com/logstash/logstash-logback-encoder#standard-fields) /// /// The allowed types are: /// * `stdout`: The logs are sent to standard output. There are no additional options. /// * `stderr`: The logs are sent to standard error output. There are no additional options. /// * `file`: Logs are written to a file. The file is reopened every time a configuration is /// re-read (therefore every time the application gets `SIGHUP`), which makes it work with /// logrotate. /// - `filename`: The path to the file where to put the logs. /// * `network`: The application connects to a given host and port over TCP and sends logs there. /// - `host`: The hostname (or IP address) to connect to. /// - `port`: The port to use. /// * `syslog`: Sends the logs to syslog. This ignores all the formatting and time options, as /// syslog handles this itself. This depends on the `to-syslog` feature. #[derive(Clone, Debug, Default, Deserialize, Serialize)] #[cfg_attr(feature = "cfg-help", derive(StructDoc))] #[serde(transparent)] pub struct Cfg(Vec<Logger>); struct Configured; impl Cfg { /// This provides an [`Extension`] to initialize logging. /// /// It calls [`init`] and sets up a basic logger (`WARN` and more serious going to `stderr`). /// /// This is seldom used directly (but can be), the [`LogInstaller`] makes sure it is called. pub fn init_extension<E: Extensible>() -> impl Extension<E> { |mut e: E| { if e.singleton::<Configured>() { init(); let logger = Logger { destination: LogDestination::StdErr, level: LevelFilterSerde(LevelFilter::Warn), per_module: HashMap::new(), clock: Clock::Local, time_format: cmdline_time_format(), format: Format::Short, }; install(create(iter::once(&logger)).unwrap()); } e } } /// Checks if the logging configuration is empty. /// /// Can be used for skipping serialization of the config array if empty. pub fn is_empty(&self) -> bool { self.0.is_empty() } } static INIT_CALLED: AtomicBool = AtomicBool::new(false); /// Initialize the global state. /// /// This installs a global logger that can be replaced at runtime and sets a panic hook to also log /// panics (see [`log_panics`]). /// /// This allows calling [`install`] later on. /// /// It is needed only if the crate is used in the manual way. This is taken care of if used through /// the [Pipeline][spirit::Pipeline]. pub fn init() { log_panics::init(); let _ = log_reroute::init(); INIT_CALLED.store(true, Ordering::Relaxed); } /// Replace the current logger with the provided one. /// /// This is a lower-level alternative to [`install`]. This allows putting an arbitrary logger in /// (with the corresponding log level at which it makes sense to try log the messages). pub fn install_parts(level: LevelFilter, logger: Box<dyn Log>) { assert!( INIT_CALLED.load(Ordering::Relaxed), "spirit_log::init not called yet" ); let actual_level = cmp::min(level, STATIC_MAX_LEVEL); log::set_max_level(actual_level); log_reroute::reroute_boxed(logger); debug!( "Installed loggers with global level filter {:?} (compiled with {:?}, runtime config {:?})", actual_level, STATIC_MAX_LEVEL, level, ); } /// Replace the current logger with the provided one. /// /// This can be called multiple times (unlike [`Dispatch::apply`]) and it always replaces the old /// logger with a new one. /// /// The logger will be installed in a synchronous way ‒ a call to logging functions may block. /// /// # Panics /// /// If [`init`] haven't been called yet. pub fn install(logger: Dispatch) { let (level, logger) = logger.into_log(); install_parts(level, logger); } impl Fragment for Cfg { type Driver = TrivialDriver; type Seed = (); type Resource = Dispatch; type Installer = LogInstaller; fn make_seed(&self, _name: &str) -> Result<(), AnyError> { Ok(()) } fn make_resource(&self, _: &mut (), _name: &str) -> Result<Dispatch, AnyError> { create(&self.0) } } /// A combination of [`Cfg`] and [`Opts`]. /// /// This is a composed [`Fragment`] ‒ the purpose is the caller can combine configuration both from /// command line options and configuration inside the same [`Pipeline`][spirit::Pipeline] ‒ see the /// [crate examples](index.html#examples). /// /// The [`Fragment`] will then combine the options to create the relevant loggers. /// /// # Interaction on stderr /// /// There's a little twist around stderr and the interaction between the `-L` option and loggers /// set up in configuration. This is to act in a way that makes some sense ‒ in particular, we /// don't want to log to stderr twice. Therefore: /// /// * If the user specifies `-l` (or `-L`) on the command line, any stderr logger from /// configuration is skipped (the `-l` takes precedence). /// * If there are no loggers in configuration but there's no `-l`, errors are logged to stderr. /// This is the case before configuration is loaded or if it contains no loggers. We want to /// report errors *somewhere*. // TODO: Non-owned version too? #[derive(Clone, Debug)] pub struct CfgAndOpts { /// The configuration options. pub cfg: Cfg, /// The command line options. pub opts: Opts, } impl Fragment for CfgAndOpts { type Driver = TrivialDriver; type Seed = (); type Resource = Dispatch; type Installer = LogInstaller; const RUN_BEFORE_CONFIG: bool = true; fn make_seed(&self, _name: &str) -> Result<(), AnyError> { Ok(()) } fn make_resource(&self, _: &mut (), _name: &str) -> Result<Dispatch, AnyError> { let mut cmd = self.opts.logger_cfg(); // No logging at all ‒ log errors to stderr if self.cfg.0.is_empty() && cmd.is_none() { cmd = Some(Logger::default()); } create( self.cfg .0 .iter() // A command line overrides any logger to stderr in configuration. But only if it // is set at all. .filter(|l| l.destination != LogDestination::StdErr || cmd.is_none()) .chain(cmd.as_ref()), ) } } /// An [`Installer`] for the [`Dispatch`]. /// /// This is the installer used by default for installing loggers ‒ this is what you get if you use /// the [`Pipeline`][spirit::Pipeline] with [`Cfg`]. /// /// Loggers installed this way act in a synchronous way ‒ they block on IO. /// /// Note that it is possible to install other loggers through this too ‒ even the async ones from /// [`Background`] transformation. #[derive(Copy, Clone, Debug, Default, Eq, PartialEq, Ord, PartialOrd, Hash)] pub struct LogInstaller; impl<O, C> Installer<Dispatch, O, C> for LogInstaller { type UninstallHandle = (); fn install(&mut self, logger: Dispatch, _: &str) { install(logger); } fn init<B: Extensible<Ok = B>>(&mut self, builder: B, _name: &str) -> Result<B, AnyError> { builder.with(Cfg::init_extension()) } } impl<O, C> Installer<(LevelFilter, Box<dyn Log>), O, C> for LogInstaller { type UninstallHandle = (); fn install(&mut self, (level, logger): (LevelFilter, Box<dyn Log>), _: &str) { install_parts(level, logger); } fn init<B: Extensible<Ok = B>>(&mut self, builder: B, _name: &str) -> Result<B, AnyError> { builder.with(Cfg::init_extension()) } }