1use anyhow::{Context, Result, anyhow};
4use derive_deftly::Deftly;
5use fs_mistrust::Mistrust;
6use serde::{Deserialize, Serialize};
7use std::io::IsTerminal as _;
8use std::path::Path;
9use std::str::FromStr;
10use tor_basic_utils::PathExt as _;
11use tor_config::ConfigBuildError;
12use tor_config_shared::opentelemetry::{OpentelemetryConfig, OpentelemetryConfigBuilder};
13use tor_config::derive::prelude::*;
14use tor_config_path::{CfgPath, CfgPathResolver};
15use tor_error::warn_report;
16use tracing::{Subscriber, error};
17use tracing_appender::non_blocking::WorkerGuard;
18use tracing_subscriber::layer::SubscriberExt;
19use tracing_subscriber::prelude::*;
20use tracing_subscriber::{Layer, filter::Targets, fmt, registry};
21
22mod fields;
23mod time;
24
25#[derive(Debug, Clone, Deftly, Eq, PartialEq)]
27#[derive_deftly(TorConfig)]
28#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
29#[cfg_attr(feature = "experimental-api", deftly(tor_config(vis = pub)))]
30pub(crate) struct LoggingConfig {
31 #[deftly(tor_config(default = default_console_filter()))]
38 console: Option<String>,
39
40 #[deftly(tor_config(
44 build = { |this: &Self| tor_config::resolve_option(&this.journald, || None) }
45 ))]
46 journald: Option<String>,
47
48 #[deftly(tor_config(
52 cfg = { all(feature = "syslog", unix) },
53 cfg_desc = "with syslog support",
54 default = Some("".into()),
55 ))]
56 syslog: Option<String>,
57
58 #[deftly(tor_config(
60 sub_builder,
61 cfg = { feature = "opentelemetry" },
62 cfg_desc = "with opentelemetry support"
63 ))]
64 opentelemetry: OpentelemetryConfig,
65
66 #[deftly(tor_config(
68 sub_builder,
69 cfg = { feature = "tokio-console" },
70 cfg_desc = "with tokio-console support"
71 ))]
72 tokio_console: TokioConsoleConfig,
73
74 #[deftly(tor_config(list(element(build), listtype = LogfileList), default = vec![]))]
78 files: Vec<LogfileConfig>,
79
80 #[deftly(tor_config(default))]
92 log_sensitive_information: bool,
93
94 #[deftly(tor_config(default))]
96 protocol_warnings: bool,
97
98 #[deftly(tor_config(default = std::time::Duration::new(1,0)))]
108 time_granularity: std::time::Duration,
109}
110
111#[allow(clippy::unnecessary_wraps)]
113fn default_console_filter() -> Option<String> {
114 Some("info".to_owned())
115}
116
117#[derive(Debug, Deftly, Clone, Eq, PartialEq)]
119#[derive_deftly(TorConfig)]
120#[deftly(tor_config(no_default_trait))]
121#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
122#[cfg_attr(feature = "experimental-api", deftly(tor_config(vis = pub)))]
123pub(crate) struct LogfileConfig {
124 #[deftly(tor_config(default))]
126 rotate: LogRotation,
127 #[deftly(tor_config(no_default))]
129 path: CfgPath,
130 #[deftly(tor_config(no_default))]
132 filter: String,
133}
134
135#[derive(Debug, Default, Clone, Serialize, Deserialize, Copy, Eq, PartialEq)]
137#[non_exhaustive]
138#[serde(rename_all = "lowercase")]
139#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
140pub(crate) enum LogRotation {
141 Daily,
143 Hourly,
145 #[default]
147 Never,
148}
149
150#[derive(Debug, Deftly, Copy, Clone, Eq, PartialEq, Serialize, Deserialize)]
152#[derive_deftly(TorConfig)]
153#[cfg(feature = "tokio-console")]
154#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
155#[cfg_attr(feature = "experimental-api", deftly(tor_config(vis = pub)))]
156pub(crate) struct TokioConsoleConfig {
157 #[deftly(tor_config(default))]
162 enabled: bool,
163}
164
165#[cfg(not(feature = "tokio-console"))]
167type TokioConsoleConfig = ();
168
169fn filt_from_str_verbose(s: &str, source: &str) -> Result<Targets> {
174 Targets::from_str(s).with_context(|| format!("in {}", source))
175}
176
177fn filt_from_opt_str(s: &Option<String>, source: &str) -> Result<Option<Targets>> {
180 Ok(match s {
181 Some(s) if !s.is_empty() => Some(filt_from_str_verbose(s, source)?),
182 _ => None,
183 })
184}
185
186fn console_layer<S>(config: &LoggingConfig, cli: Option<&str>) -> Result<impl Layer<S> + use<S>>
188where
189 S: Subscriber + for<'span> tracing_subscriber::registry::LookupSpan<'span>,
190{
191 let timer = time::new_formatter(config.time_granularity);
192 let filter = cli
193 .map(|s| filt_from_str_verbose(s, "--log-level command line parameter"))
194 .or_else(|| filt_from_opt_str(&config.console, "logging.console").transpose())
195 .unwrap_or_else(|| Ok(Targets::from_str("debug").expect("bad default")))?;
196 let use_color = std::io::stderr().is_terminal();
197 Ok(fmt::Layer::default()
202 .fmt_fields(fields::ErrorsLastFieldFormatter)
204 .with_ansi(use_color)
205 .with_timer(timer)
206 .with_writer(std::io::stderr) .with_filter(filter))
208}
209
210#[cfg(feature = "journald")]
213fn journald_layer<S>(config: &LoggingConfig) -> Result<impl Layer<S>>
214where
215 S: Subscriber + for<'span> tracing_subscriber::registry::LookupSpan<'span>,
216{
217 if let Some(filter) = filt_from_opt_str(&config.journald, "logging.journald")? {
218 Ok(Some(tracing_journald::layer()?.with_filter(filter)))
219 } else {
220 Ok(None)
222 }
223}
224
225#[cfg(all(feature = "syslog", unix))]
228fn syslog_layer<S>(config: &LoggingConfig) -> Result<impl Layer<S>>
229where
230 S: Subscriber + for<'span> tracing_subscriber::registry::LookupSpan<'span>,
231{
232 use syslog_tracing::{Facility, Options, Syslog};
233
234 let identity = c"arti";
235
236 if let Some(filter) = filt_from_opt_str(&config.syslog, "logging.syslog")? {
237 let options = Options::LOG_PID;
238 let facility = Facility::Daemon;
239
240 let syslog_maker = Syslog::new(identity, options, facility).ok_or_else(|| {
241 anyhow::anyhow!("syslog already initialized; only one logger allowed")
242 })?;
243
244 let layer = tracing_subscriber::fmt::layer()
245 .with_writer(syslog_maker)
246 .with_ansi(false)
249 .without_time()
250 .with_filter(filter);
251
252 Ok(Some(layer))
253 } else {
254 Ok(None)
255 }
256}
257
258#[cfg(feature = "opentelemetry")]
263fn otel_layer<S>(config: &LoggingConfig, path_resolver: &CfgPathResolver) -> Result<impl Layer<S>>
264where
265 S: Subscriber + for<'span> tracing_subscriber::registry::LookupSpan<'span>,
266{
267 use opentelemetry::trace::TracerProvider;
268 use opentelemetry_otlp::WithExportConfig;
269
270 if config.opentelemetry.file().is_some() && config.opentelemetry.http().is_some() {
271 return Err(ConfigBuildError::Invalid {
272 field: "logging.opentelemetry".into(),
273 problem: "Only one OpenTelemetry exporter can be enabled at once.".into(),
274 }
275 .into());
276 }
277
278 let resource = opentelemetry_sdk::Resource::builder()
279 .with_service_name("arti")
280 .build();
281
282 let span_processor = if let Some(otel_file_config) = &config.opentelemetry.file() {
283 let file = std::fs::File::options()
284 .create(true)
285 .append(true)
286 .open(otel_file_config.path().path(path_resolver)?)?;
287
288 let exporter = otlp_file_exporter::FileExporter::new(file, resource.clone());
289
290 opentelemetry_sdk::trace::BatchSpanProcessor::builder(exporter)
291 .with_batch_config(otel_file_config.batch().clone().into())
292 .build()
293 } else if let Some(otel_http_config) = &config.opentelemetry.http() {
294 if otel_http_config.endpoint().starts_with("http://")
295 && !(otel_http_config.endpoint().starts_with("http://localhost")
296 || otel_http_config.endpoint().starts_with("http://127.0.0.1"))
297 {
298 return Err(ConfigBuildError::Invalid {
299 field: "logging.opentelemetry.http.endpoint".into(),
300 problem: "OpenTelemetry endpoint is set to HTTP on a non-localhost address! For security reasons, this is not supported.".into(),
301 }
302 .into());
303 }
304 let exporter = opentelemetry_otlp::SpanExporter::builder()
305 .with_http()
306 .with_endpoint(otel_http_config.endpoint().clone())
307 .with_timeout(*otel_http_config.timeout())
308 .build()?;
309
310 opentelemetry_sdk::trace::BatchSpanProcessor::builder(exporter)
311 .with_batch_config(otel_http_config.batch().clone().into())
312 .build()
313 } else {
314 return Ok(None);
315 };
316
317 let tracer_provider = opentelemetry_sdk::trace::SdkTracerProvider::builder()
318 .with_resource(resource.clone())
319 .with_span_processor(span_processor)
320 .build();
321
322 let tracer = tracer_provider.tracer("otel_file_tracer");
323
324 Ok(Some(tracing_opentelemetry::layer().with_tracer(tracer)))
325}
326
327fn logfile_layer<S>(
333 config: &LogfileConfig,
334 granularity: std::time::Duration,
335 mistrust: &Mistrust,
336 path_resolver: &CfgPathResolver,
337) -> Result<(impl Layer<S> + Send + Sync + Sized + use<S>, WorkerGuard)>
338where
339 S: Subscriber + for<'span> tracing_subscriber::registry::LookupSpan<'span> + Send + Sync,
340{
341 use tracing_appender::{
342 non_blocking,
343 rolling::{RollingFileAppender, Rotation},
344 };
345 let timer = time::new_formatter(granularity);
346
347 let filter = filt_from_str_verbose(&config.filter, "logging.files.filter")?;
348 let rotation = match config.rotate {
349 LogRotation::Daily => Rotation::DAILY,
350 LogRotation::Hourly => Rotation::HOURLY,
351 _ => Rotation::NEVER,
352 };
353 let path = config.path.path(path_resolver)?;
354
355 let directory = match path.parent() {
356 None => {
357 return Err(anyhow!(
358 "Logfile path \"{}\" did not have a parent directory",
359 path.display_lossy()
360 ));
361 }
362 Some(p) if p == Path::new("") => Path::new("."),
363 Some(d) => d,
364 };
365 mistrust.make_directory(directory).with_context(|| {
366 format!(
367 "Unable to create parent directory for logfile \"{}\"",
368 path.display_lossy()
369 )
370 })?;
371 let fname = path
372 .file_name()
373 .ok_or_else(|| anyhow!("No path for log file"))
374 .map(Path::new)?;
375
376 let appender = RollingFileAppender::new(rotation, directory, fname);
377 let (nonblocking, guard) = non_blocking(appender);
378 let layer = fmt::layer()
379 .fmt_fields(fields::ErrorsLastFieldFormatter)
381 .with_ansi(false)
382 .with_writer(nonblocking)
383 .with_timer(timer)
384 .with_filter(filter);
385 Ok((layer, guard))
386}
387
388fn logfile_layers<S>(
393 config: &LoggingConfig,
394 mistrust: &Mistrust,
395 path_resolver: &CfgPathResolver,
396) -> Result<(impl Layer<S> + use<S>, Vec<WorkerGuard>)>
397where
398 S: Subscriber + for<'span> tracing_subscriber::registry::LookupSpan<'span> + Send + Sync,
399{
400 let mut guards = Vec::new();
401 if config.files.is_empty() {
402 return Ok((None, guards));
405 }
406
407 let (layer, guard) = logfile_layer(
408 &config.files[0],
409 config.time_granularity,
410 mistrust,
411 path_resolver,
412 )?;
413 guards.push(guard);
414
415 let mut layer: Box<dyn Layer<S> + Send + Sync + 'static> = Box::new(layer);
418
419 for logfile in &config.files[1..] {
420 let (new_layer, guard) =
421 logfile_layer(logfile, config.time_granularity, mistrust, path_resolver)?;
422 layer = Box::new(layer.and_then(new_layer));
423 guards.push(guard);
424 }
425
426 Ok((Some(layer), guards))
427}
428
429fn install_panic_handler() {
432 let default_handler = std::panic::take_hook();
438 std::panic::set_hook(Box::new(move |panic_info| {
439 default_handler(panic_info);
442
443 let msg = match panic_info.payload().downcast_ref::<&'static str>() {
445 Some(s) => *s,
446 None => match panic_info.payload().downcast_ref::<String>() {
447 Some(s) => s.as_str(),
448 None => "Box<dyn Any>",
449 },
450 };
451
452 let backtrace = std::backtrace::Backtrace::force_capture();
453 match panic_info.location() {
454 Some(location) => error!("Panic at {}: {}\n{}", location, msg, backtrace),
455 None => error!("Panic at ???: {}\n{}", msg, backtrace),
456 };
457 }));
458}
459
460#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
463pub(crate) struct LogGuards {
464 #[allow(unused)]
466 guards: Vec<WorkerGuard>,
467
468 #[allow(unused)]
470 safelog_guard: Option<safelog::Guard>,
471}
472
473#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
478#[cfg_attr(docsrs, doc(cfg(feature = "experimental-api")))]
479pub(crate) fn setup_logging(
480 config: &LoggingConfig,
481 mistrust: &Mistrust,
482 path_resolver: &CfgPathResolver,
483 cli: Option<&str>,
484) -> Result<LogGuards> {
485 let registry = registry().with(console_layer(config, cli)?);
494
495 #[cfg(feature = "journald")]
496 let registry = registry.with(journald_layer(config)?);
497
498 #[cfg(all(feature = "syslog", unix))]
499 let registry = registry.with(syslog_layer(config)?);
500
501 #[cfg(feature = "opentelemetry")]
502 let registry = registry.with(otel_layer(config, path_resolver)?);
503
504 #[cfg(feature = "tokio-console")]
505 let registry = {
506 let tokio_layer = if config.tokio_console.enabled {
513 Some(console_subscriber::spawn())
514 } else {
515 None
516 };
517 registry.with(tokio_layer)
518 };
519
520 let (layer, guards) = logfile_layers(config, mistrust, path_resolver)?;
521 let registry = registry.with(layer);
522
523 registry.init();
524
525 let safelog_guard = if config.log_sensitive_information {
526 match safelog::disable_safe_logging() {
527 Ok(guard) => Some(guard),
528 Err(e) => {
529 warn_report!(e, "Unable to disable safe logging");
532 None
533 }
534 }
535 } else {
536 None
537 };
538
539 let mode = if config.protocol_warnings {
540 tor_error::tracing::ProtocolWarningMode::Warn
541 } else {
542 tor_error::tracing::ProtocolWarningMode::Off
543 };
544 tor_error::tracing::set_protocol_warning_mode(mode);
545
546 install_panic_handler();
547
548 Ok(LogGuards {
549 guards,
550 safelog_guard,
551 })
552}