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}