arti/
logging.rs

1//! Configure tracing subscribers for Arti
2
3use anyhow::{anyhow, Context, Result};
4use derive_builder::Builder;
5use fs_mistrust::Mistrust;
6use serde::{Deserialize, Serialize};
7use std::io::IsTerminal as _;
8use std::path::Path;
9use std::str::FromStr;
10use tor_config::impl_standard_builder;
11use tor_config::ConfigBuildError;
12use tor_config::{define_list_builder_accessors, define_list_builder_helper};
13use tor_config_path::{CfgPath, CfgPathResolver};
14use tor_error::warn_report;
15use tracing::{error, Subscriber};
16use tracing_appender::non_blocking::WorkerGuard;
17use tracing_subscriber::layer::SubscriberExt;
18use tracing_subscriber::prelude::*;
19use tracing_subscriber::{filter::Targets, fmt, registry, Layer};
20
21mod time;
22
23/// Structure to hold our logging configuration options
24#[derive(Debug, Clone, Builder, Eq, PartialEq)]
25#[non_exhaustive] // TODO(nickm) remove public elements when I revise this.
26#[builder(build_fn(error = "ConfigBuildError"))]
27#[builder(derive(Debug, Serialize, Deserialize))]
28pub struct LoggingConfig {
29    /// Filtering directives that determine tracing levels as described at
30    /// <https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/targets/struct.Targets.html#impl-FromStr>
31    ///
32    /// You can override this setting with the -l, --log-level command line parameter.
33    ///
34    /// Example: "info,tor_proto::channel=trace"
35    #[builder(default = "default_console_filter()", setter(into, strip_option))]
36    console: Option<String>,
37
38    /// Filtering directives for the journald logger.
39    ///
40    /// Only takes effect if Arti is built with the `journald` filter.
41    #[builder(
42        setter(into),
43        field(build = r#"tor_config::resolve_option(&self.journald, || None)"#)
44    )]
45    journald: Option<String>,
46
47    /// Configuration for one or more logfiles.
48    ///
49    /// The default is not to log to any files.
50    #[builder_field_attr(serde(default))]
51    #[builder(sub_builder, setter(custom))]
52    files: LogfileListConfig,
53
54    /// If set to true, we disable safe logging on _all logs_, and store
55    /// potentially sensitive information at level `info` or higher.
56    ///
57    /// This can be useful for debugging, but it increases the value of your
58    /// logs to an attacker.  Do not turn this on in production unless you have
59    /// a good log rotation mechanism.
60    //
61    // TODO: Eventually we might want to make this more complex, and add a
62    // per-log mechanism to turn off unsafe logging. Alternatively, we might do
63    // that by extending the filter syntax implemented by `tracing` to have an
64    // "unsafe" flag on particular lines.
65    #[builder_field_attr(serde(default))]
66    #[builder(default)]
67    log_sensitive_information: bool,
68
69    /// An approximate granularity with which log times should be displayed.
70    ///
71    /// This value controls every log time that arti outputs; it doesn't have any
72    /// effect on times written by other logging programs like `journald`.
73    ///
74    /// We may round this value up for convenience: For example, if you say
75    /// "2.5s", we may treat it as if you had said "3s."
76    ///
77    /// The default is "1s", or one second.
78    #[builder(default = "std::time::Duration::new(1,0)")]
79    #[builder_field_attr(serde(default, with = "humantime_serde::option"))]
80    time_granularity: std::time::Duration,
81}
82impl_standard_builder! { LoggingConfig }
83
84/// Return a default tracing filter value for `logging.console`.
85#[allow(clippy::unnecessary_wraps)]
86fn default_console_filter() -> Option<String> {
87    Some("info".to_owned())
88}
89
90/// Local type alias, mostly helpful for derive_builder to DTRT
91type LogfileListConfig = Vec<LogfileConfig>;
92
93define_list_builder_helper! {
94    struct LogfileListConfigBuilder {
95        files: [LogfileConfigBuilder],
96    }
97    built: LogfileListConfig = files;
98    default = vec![];
99}
100
101define_list_builder_accessors! {
102    struct LoggingConfigBuilder {
103        pub files: [LogfileConfigBuilder],
104    }
105}
106
107/// Configuration information for an (optionally rotating) logfile.
108#[derive(Debug, Builder, Clone, Eq, PartialEq)]
109#[builder(derive(Debug, Serialize, Deserialize))]
110#[builder(build_fn(error = "ConfigBuildError"))]
111pub struct LogfileConfig {
112    /// How often to rotate the file?
113    #[builder(default)]
114    rotate: LogRotation,
115    /// Where to write the files?
116    path: CfgPath,
117    /// Filter to apply before writing
118    filter: String,
119}
120
121impl_standard_builder! { LogfileConfig: !Default }
122
123/// How often to rotate a log file
124#[derive(Debug, Default, Clone, Serialize, Deserialize, Copy, Eq, PartialEq)]
125#[non_exhaustive]
126#[serde(rename_all = "lowercase")]
127pub enum LogRotation {
128    /// Rotate logs daily
129    Daily,
130    /// Rotate logs hourly
131    Hourly,
132    /// Never rotate the log
133    #[default]
134    Never,
135}
136
137/// As [`Targets::from_str`], but wrapped in an [`anyhow::Result`].
138//
139// (Note that we have to use `Targets`, not `EnvFilter`: see comment in
140// `setup_logging()`.)
141fn filt_from_str_verbose(s: &str, source: &str) -> Result<Targets> {
142    Targets::from_str(s).with_context(|| format!("in {}", source))
143}
144
145/// As filt_from_str_verbose, but treat an absent filter (or an empty string) as
146/// None.
147fn filt_from_opt_str(s: &Option<String>, source: &str) -> Result<Option<Targets>> {
148    Ok(match s {
149        Some(s) if !s.is_empty() => Some(filt_from_str_verbose(s, source)?),
150        _ => None,
151    })
152}
153
154/// Try to construct a tracing [`Layer`] for logging to stdout.
155fn console_layer<S>(config: &LoggingConfig, cli: Option<&str>) -> Result<impl Layer<S>>
156where
157    S: Subscriber + for<'span> tracing_subscriber::registry::LookupSpan<'span>,
158{
159    let timer = time::new_formatter(config.time_granularity);
160    let filter = cli
161        .map(|s| filt_from_str_verbose(s, "--log-level command line parameter"))
162        .or_else(|| filt_from_opt_str(&config.console, "logging.console").transpose())
163        .unwrap_or_else(|| Ok(Targets::from_str("debug").expect("bad default")))?;
164    let use_color = std::io::stdout().is_terminal();
165    // We used to suppress safe-logging on the console, but we removed that
166    // feature: we cannot be certain that the console really is volatile. Even
167    // if isatty() returns true on the console, we can't be sure that the
168    // terminal isn't saving backlog to disk or something like that.
169    Ok(fmt::Layer::default()
170        .with_ansi(use_color)
171        .with_timer(timer)
172        .with_writer(std::io::stdout) // we make this explicit, to match with use_color.
173        .with_filter(filter))
174}
175
176/// Try to construct a tracing [`Layer`] for logging to journald, if one is
177/// configured.
178#[cfg(feature = "journald")]
179fn journald_layer<S>(config: &LoggingConfig) -> Result<impl Layer<S>>
180where
181    S: Subscriber + for<'span> tracing_subscriber::registry::LookupSpan<'span>,
182{
183    if let Some(filter) = filt_from_opt_str(&config.journald, "logging.journald")? {
184        Ok(Some(tracing_journald::layer()?.with_filter(filter)))
185    } else {
186        // Fortunately, Option<Layer> implements Layer, so we can just return None here.
187        Ok(None)
188    }
189}
190
191/// Try to construct a non-blocking tracing [`Layer`] for writing data to an
192/// optionally rotating logfile.
193///
194/// On success, return that layer, along with a WorkerGuard that needs to be
195/// dropped when the program exits, to flush buffered messages.
196fn logfile_layer<S>(
197    config: &LogfileConfig,
198    granularity: std::time::Duration,
199    mistrust: &Mistrust,
200    path_resolver: &CfgPathResolver,
201) -> Result<(impl Layer<S> + Send + Sync + Sized, WorkerGuard)>
202where
203    S: Subscriber + for<'span> tracing_subscriber::registry::LookupSpan<'span> + Send + Sync,
204{
205    use tracing_appender::{
206        non_blocking,
207        rolling::{RollingFileAppender, Rotation},
208    };
209    let timer = time::new_formatter(granularity);
210
211    let filter = filt_from_str_verbose(&config.filter, "logging.files.filter")?;
212    let rotation = match config.rotate {
213        LogRotation::Daily => Rotation::DAILY,
214        LogRotation::Hourly => Rotation::HOURLY,
215        _ => Rotation::NEVER,
216    };
217    let path = config.path.path(path_resolver)?;
218    let directory = path.parent().unwrap_or_else(|| Path::new("."));
219    mistrust.make_directory(directory)?;
220    let fname = path
221        .file_name()
222        .ok_or_else(|| anyhow!("No path for log file"))
223        .map(Path::new)?;
224
225    let appender = RollingFileAppender::new(rotation, directory, fname);
226    let (nonblocking, guard) = non_blocking(appender);
227    let layer = fmt::layer()
228        .with_ansi(false)
229        .with_writer(nonblocking)
230        .with_timer(timer)
231        .with_filter(filter);
232    Ok((layer, guard))
233}
234
235/// Try to construct a tracing [`Layer`] for all of the configured logfiles.
236///
237/// On success, return that layer along with a list of [`WorkerGuard`]s that
238/// need to be dropped when the program exits.
239fn logfile_layers<S>(
240    config: &LoggingConfig,
241    mistrust: &Mistrust,
242    path_resolver: &CfgPathResolver,
243) -> Result<(impl Layer<S>, Vec<WorkerGuard>)>
244where
245    S: Subscriber + for<'span> tracing_subscriber::registry::LookupSpan<'span> + Send + Sync,
246{
247    let mut guards = Vec::new();
248    if config.files.is_empty() {
249        // As above, we have Option<Layer> implements Layer, so we can return
250        // None in this case.
251        return Ok((None, guards));
252    }
253
254    let (layer, guard) = logfile_layer(
255        &config.files[0],
256        config.time_granularity,
257        mistrust,
258        path_resolver,
259    )?;
260    guards.push(guard);
261
262    // We have to use a dyn pointer here so we can build up linked list of
263    // arbitrary depth.
264    let mut layer: Box<dyn Layer<S> + Send + Sync + 'static> = Box::new(layer);
265
266    for logfile in &config.files[1..] {
267        let (new_layer, guard) =
268            logfile_layer(logfile, config.time_granularity, mistrust, path_resolver)?;
269        layer = Box::new(layer.and_then(new_layer));
270        guards.push(guard);
271    }
272
273    Ok((Some(layer), guards))
274}
275
276/// Configure a panic handler to send everything to tracing, in addition to our
277/// default panic behavior.
278fn install_panic_handler() {
279    // TODO library support: There's a library called `tracing-panic` that
280    // provides a hook we could use instead, but that doesn't have backtrace
281    // support.  We should consider using it if it gets backtrace support in the
282    // future.  We should also keep an eye on `tracing` to see if it learns how
283    // to do this for us.
284    let default_handler = std::panic::take_hook();
285    std::panic::set_hook(Box::new(move |panic_info| {
286        // Note that if we were ever to _not_ call this handler,
287        // we would want to abort on nested panics and !can_unwind cases.
288        default_handler(panic_info);
289
290        // This statement is copied from stdlib.
291        let msg = match panic_info.payload().downcast_ref::<&'static str>() {
292            Some(s) => *s,
293            None => match panic_info.payload().downcast_ref::<String>() {
294                Some(s) => &s[..],
295                None => "Box<dyn Any>",
296            },
297        };
298
299        let backtrace = std::backtrace::Backtrace::force_capture();
300        match panic_info.location() {
301            Some(location) => error!("Panic at {}: {}\n{}", location, msg, backtrace),
302            None => error!("Panic at ???: {}\n{}", msg, backtrace),
303        };
304    }));
305}
306
307/// Opaque structure that gets dropped when the program is shutting down,
308/// after logs are no longer needed.  The `Drop` impl flushes buffered messages.
309#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
310pub(crate) struct LogGuards {
311    /// The actual list of guards we're returning.
312    #[allow(unused)]
313    guards: Vec<WorkerGuard>,
314
315    /// A safelog guard, for use if we have decided to disable safe logging.
316    #[allow(unused)]
317    safelog_guard: Option<safelog::Guard>,
318}
319
320/// Set up logging.
321///
322/// Note that the returned LogGuard must be dropped precisely when the program
323/// quits; they're used to ensure that all the log messages are flushed.
324#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
325#[cfg_attr(docsrs, doc(cfg(feature = "experimental-api")))]
326pub(crate) fn setup_logging(
327    config: &LoggingConfig,
328    mistrust: &Mistrust,
329    path_resolver: &CfgPathResolver,
330    cli: Option<&str>,
331) -> Result<LogGuards> {
332    // Important: We have to make sure that the individual layers we add here
333    // are not filters themselves.  That means, for example, that we can't add
334    // an `EnvFilter` layer unless we want it to apply globally to _all_ layers.
335    //
336    // For a bit of discussion on the difference between per-layer filters and filters
337    // that apply to the entire registry, see
338    // https://docs.rs/tracing-subscriber/0.3.5/tracing_subscriber/layer/index.html#global-filtering
339
340    let registry = registry().with(console_layer(config, cli)?);
341
342    #[cfg(feature = "journald")]
343    let registry = registry.with(journald_layer(config)?);
344
345    let (layer, guards) = logfile_layers(config, mistrust, path_resolver)?;
346    let registry = registry.with(layer);
347
348    registry.init();
349
350    let safelog_guard = if config.log_sensitive_information {
351        match safelog::disable_safe_logging() {
352            Ok(guard) => Some(guard),
353            Err(e) => {
354                // We don't need to propagate this error; it isn't the end of
355                // the world if we were unable to disable safe logging.
356                warn_report!(e, "Unable to disable safe logging");
357                None
358            }
359        }
360    } else {
361        None
362    };
363
364    install_panic_handler();
365
366    Ok(LogGuards {
367        guards,
368        safelog_guard,
369    })
370}