1
//! Configure tracing subscribers for Arti
2

            
3
use anyhow::{anyhow, Context, Result};
4
use arti_config::{LogRotation, LogfileConfig, LoggingConfig};
5
use std::path::Path;
6
use std::str::FromStr;
7
use tracing::Subscriber;
8
use tracing_appender::non_blocking::WorkerGuard;
9
use tracing_subscriber::layer::SubscriberExt;
10
use tracing_subscriber::prelude::*;
11
use tracing_subscriber::{filter::Targets, fmt, registry, Layer};
12

            
13
/// As [`Targets::from_str`], but wrapped in an [`anyhow::Result`].
14
//
15
// (Note that we have to use `Targets`, not `EnvFilter`: see comment in
16
// `setup_logging()`.)
17
fn filt_from_str_verbose(s: &str, source: &str) -> Result<Targets> {
18
    Targets::from_str(s).with_context(|| format!("in {}", source))
19
}
20

            
21
/// As filt_from_str_verbose, but treat an absent filter (or an empty string) as
22
/// None.
23
fn filt_from_opt_str(s: Option<&str>, source: &str) -> Result<Option<Targets>> {
24
    s.map(|s| filt_from_str_verbose(s, source)).transpose()
25
}
26

            
27
/// Try to construct a tracing [`Layer`] for logging to stdout.
28
fn console_layer<S>(config: &LoggingConfig, cli: Option<&str>) -> Result<impl Layer<S>>
29
where
30
    S: Subscriber + for<'span> tracing_subscriber::registry::LookupSpan<'span>,
31
{
32
    let filter = cli
33
        .map(|s| filt_from_str_verbose(s, "--log-level command line parameter"))
34
        .or_else(|| filt_from_opt_str(config.console_filter(), "logging.console").transpose())
35
        .unwrap_or_else(|| Ok(Targets::from_str("debug").expect("bad default")))?;
36
    Ok(fmt::Layer::default().with_filter(filter))
37
}
38

            
39
/// Try to construct a tracing [`Layer`] for logging to journald, if one is
40
/// configured.
41
#[cfg(feature = "journald")]
42
fn journald_layer<S>(config: &LoggingConfig) -> Result<impl Layer<S>>
43
where
44
    S: Subscriber + for<'span> tracing_subscriber::registry::LookupSpan<'span>,
45
{
46
    if let Some(filter) = filt_from_opt_str(config.journald_filter(), "logging.journald")? {
47
        Ok(Some(tracing_journald::layer()?.with_filter(filter)))
48
    } else {
49
        // Fortunately, Option<Layer> implements Layer, so we can just return None here.
50
        Ok(None)
51
    }
52
}
53

            
54
/// Try to construct a non-blocking tracing [`Layer`] for writing data to an
55
/// optionally rotating logfile.
56
///
57
/// On success, return that layer, along with a WorkerGuard that needs to be
58
/// dropped when the program exits, to flush buffered messages.
59
fn logfile_layer<S>(
60
    config: &LogfileConfig,
61
) -> Result<(impl Layer<S> + Send + Sync + Sized, WorkerGuard)>
62
where
63
    S: Subscriber + for<'span> tracing_subscriber::registry::LookupSpan<'span> + Send + Sync,
64
{
65
    use tracing_appender::{
66
        non_blocking,
67
        rolling::{RollingFileAppender, Rotation},
68
    };
69

            
70
    let filter = filt_from_str_verbose(config.filter(), "logging.files.filter")?;
71
    let rotation = match config.rotate() {
72
        LogRotation::Daily => Rotation::DAILY,
73
        LogRotation::Hourly => Rotation::HOURLY,
74
        _ => Rotation::NEVER,
75
    };
76
    let path = config.path().path()?;
77
    let directory = path.parent().unwrap_or_else(|| Path::new("."));
78
    let fname = path
79
        .file_name()
80
        .ok_or_else(|| anyhow!("No path for log file"))
81
        .map(Path::new)?;
82

            
83
    let appender = RollingFileAppender::new(rotation, directory, fname);
84
    let (nonblocking, guard) = non_blocking(appender);
85
    let layer = fmt::layer().with_writer(nonblocking).with_filter(filter);
86
    Ok((layer, guard))
87
}
88

            
89
/// Try to construct a tracing [`Layer`] for all of the configured logfiles.
90
///
91
/// On success, return that layer along with a list of [`WorkerGuard`]s that
92
/// need to be dropped when the program exits.
93
fn logfile_layers<S>(config: &LoggingConfig) -> Result<(impl Layer<S>, Vec<WorkerGuard>)>
94
where
95
    S: Subscriber + for<'span> tracing_subscriber::registry::LookupSpan<'span> + Send + Sync,
96
{
97
    let mut guards = Vec::new();
98
    if config.logfiles().is_empty() {
99
        // As above, we have Option<Layer> implements Layer, so we can return
100
        // None in this case.
101
        return Ok((None, guards));
102
    }
103

            
104
    let (layer, guard) = logfile_layer(&config.logfiles()[0])?;
105
    guards.push(guard);
106

            
107
    // We have to use a dyn pointer here so we can build up linked list of
108
    // arbitrary depth.
109
    let mut layer: Box<dyn Layer<S> + Send + Sync + 'static> = Box::new(layer);
110

            
111
    for logfile in &config.logfiles()[1..] {
112
        let (new_layer, guard) = logfile_layer(logfile)?;
113
        layer = Box::new(layer.and_then(new_layer));
114
        guards.push(guard);
115
    }
116

            
117
    Ok((Some(layer), guards))
118
}
119

            
120
/// Opaque structure that gets dropped when the program is shutting down,
121
/// after logs are no longer needed.  The `Drop` impl flushes buffered messages.
122
pub struct LogGuards {
123
    /// The actual list of guards we're returning.
124
    #[allow(unused)]
125
    guards: Vec<WorkerGuard>,
126
}
127

            
128
/// Set up logging.
129
///
130
/// Note that the returned LogGuard must be dropped precisely when the program
131
/// quits; they're used to ensure that all the log messages are flushed.
132
pub fn setup_logging(config: &LoggingConfig, cli: Option<&str>) -> Result<LogGuards> {
133
    // Important: We have to make sure that the individual layers we add here
134
    // are not filters themselves.  That means, for example, that we can't add
135
    // an `EnvFilter` layer unless we want it to apply globally to _all_ layers.
136
    //
137
    // For a bit of discussion on the difference between per-layer filters and filters
138
    // that apply to the entire registry, see
139
    // https://docs.rs/tracing-subscriber/0.3.5/tracing_subscriber/layer/index.html#global-filtering
140

            
141
    let registry = registry().with(console_layer(config, cli)?);
142

            
143
    #[cfg(feature = "journald")]
144
    let registry = registry.with(journald_layer(config)?);
145

            
146
    let (layer, guards) = logfile_layers(config)?;
147
    let registry = registry.with(layer);
148

            
149
    registry.init();
150

            
151
    Ok(LogGuards { guards })
152
}