1use 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#[derive(Debug, Clone, Builder, Eq, PartialEq)]
25#[non_exhaustive] #[builder(build_fn(error = "ConfigBuildError"))]
27#[builder(derive(Debug, Serialize, Deserialize))]
28pub struct LoggingConfig {
29 #[builder(default = "default_console_filter()", setter(into, strip_option))]
36 console: Option<String>,
37
38 #[builder(
42 setter(into),
43 field(build = r#"tor_config::resolve_option(&self.journald, || None)"#)
44 )]
45 journald: Option<String>,
46
47 #[builder_field_attr(serde(default))]
51 #[builder(sub_builder, setter(custom))]
52 files: LogfileListConfig,
53
54 #[builder_field_attr(serde(default))]
66 #[builder(default)]
67 log_sensitive_information: bool,
68
69 #[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#[allow(clippy::unnecessary_wraps)]
86fn default_console_filter() -> Option<String> {
87 Some("info".to_owned())
88}
89
90type 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#[derive(Debug, Builder, Clone, Eq, PartialEq)]
109#[builder(derive(Debug, Serialize, Deserialize))]
110#[builder(build_fn(error = "ConfigBuildError"))]
111pub struct LogfileConfig {
112 #[builder(default)]
114 rotate: LogRotation,
115 path: CfgPath,
117 filter: String,
119}
120
121impl_standard_builder! { LogfileConfig: !Default }
122
123#[derive(Debug, Default, Clone, Serialize, Deserialize, Copy, Eq, PartialEq)]
125#[non_exhaustive]
126#[serde(rename_all = "lowercase")]
127pub enum LogRotation {
128 Daily,
130 Hourly,
132 #[default]
134 Never,
135}
136
137fn filt_from_str_verbose(s: &str, source: &str) -> Result<Targets> {
142 Targets::from_str(s).with_context(|| format!("in {}", source))
143}
144
145fn 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
154fn 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 Ok(fmt::Layer::default()
170 .with_ansi(use_color)
171 .with_timer(timer)
172 .with_writer(std::io::stdout) .with_filter(filter))
174}
175
176#[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 Ok(None)
188 }
189}
190
191fn 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
235fn 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 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 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
276fn install_panic_handler() {
279 let default_handler = std::panic::take_hook();
285 std::panic::set_hook(Box::new(move |panic_info| {
286 default_handler(panic_info);
289
290 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#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
310pub(crate) struct LogGuards {
311 #[allow(unused)]
313 guards: Vec<WorkerGuard>,
314
315 #[allow(unused)]
317 safelog_guard: Option<safelog::Guard>,
318}
319
320#[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 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 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}