spirit_log/
lib.rs

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