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 std::time::Duration;
11use tor_basic_utils::PathExt as _;
12use tor_config::ConfigBuildError;
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;
23#[cfg(feature = "opentelemetry")]
24mod otlp_file_exporter;
25mod time;
26
27#[derive(Debug, Clone, Deftly, Eq, PartialEq)]
29#[derive_deftly(TorConfig)]
30#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
31#[cfg_attr(feature = "experimental-api", deftly(tor_config(vis = "pub")))]
32pub(crate) struct LoggingConfig {
33 #[deftly(tor_config(default = "default_console_filter()"))]
40 console: Option<String>,
41
42 #[deftly(tor_config(
46 build = r#"|this: &Self| tor_config::resolve_option(&this.journald, || None)"#
47 ))]
48 journald: Option<String>,
49
50 #[deftly(tor_config(
54 cfg = r#"all(feature = "syslog", unix)"#,
55 cfg_desc = "with syslog support",
56 default = r#"Some("".into())"#,
57 ))]
58 syslog: Option<String>,
59
60 #[deftly(tor_config(
62 sub_builder,
63 cfg = r#" feature = "opentelemetry" "#,
64 cfg_desc = "with opentelemetry support"
65 ))]
66 opentelemetry: OpentelemetryConfig,
67
68 #[deftly(tor_config(
70 sub_builder,
71 cfg = r#" feature = "tokio-console" "#,
72 cfg_desc = "with tokio-console support"
73 ))]
74 tokio_console: TokioConsoleConfig,
75
76 #[deftly(tor_config(list(element(build), listtype = "LogfileList"), default = "vec![]"))]
80 files: Vec<LogfileConfig>,
81
82 #[deftly(tor_config(default))]
94 log_sensitive_information: bool,
95
96 #[deftly(tor_config(default))]
98 protocol_warnings: bool,
99
100 #[deftly(tor_config(default = "std::time::Duration::new(1,0)"))]
110 time_granularity: std::time::Duration,
111}
112
113#[allow(clippy::unnecessary_wraps)]
115fn default_console_filter() -> Option<String> {
116 Some("info".to_owned())
117}
118
119#[derive(Debug, Deftly, Clone, Eq, PartialEq)]
121#[derive_deftly(TorConfig)]
122#[deftly(tor_config(no_default_trait))]
123#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
124#[cfg_attr(feature = "experimental-api", deftly(tor_config(vis = "pub")))]
125pub(crate) struct LogfileConfig {
126 #[deftly(tor_config(default))]
128 rotate: LogRotation,
129 #[deftly(tor_config(no_default))]
131 path: CfgPath,
132 #[deftly(tor_config(no_default))]
134 filter: String,
135}
136
137#[derive(Debug, Default, Clone, Serialize, Deserialize, Copy, Eq, PartialEq)]
139#[non_exhaustive]
140#[serde(rename_all = "lowercase")]
141#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
142pub(crate) enum LogRotation {
143 Daily,
145 Hourly,
147 #[default]
149 Never,
150}
151
152#[derive(Debug, Deftly, Clone, Eq, PartialEq, Serialize, Deserialize)]
154#[derive_deftly(TorConfig)]
155#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
156#[cfg_attr(feature = "experimental-api", deftly(tor_config(vis = "pub")))]
157pub(crate) struct OpentelemetryConfig {
158 #[deftly(tor_config(default))]
160 file: Option<OpentelemetryFileExporterConfig>,
161 #[deftly(tor_config(default))]
163 http: Option<OpentelemetryHttpExporterConfig>,
164}
165
166#[derive(Debug, Deftly, Clone, Eq, PartialEq, Serialize, Deserialize)]
168#[derive_deftly(TorConfig)]
169#[deftly(tor_config(no_default_trait))]
170#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
171#[cfg_attr(feature = "experimental-api", deftly(tor_config(vis = "pub")))]
172pub(crate) struct OpentelemetryHttpExporterConfig {
173 #[deftly(tor_config(no_default))]
177 endpoint: String,
178 #[deftly(tor_config(sub_builder))]
180 batch: OpentelemetryBatchConfig,
181 #[deftly(tor_config(no_magic, default))]
189 timeout: Option<Duration>,
190 }
193
194#[derive(Debug, Deftly, Clone, Eq, PartialEq, Serialize, Deserialize)]
196#[derive_deftly(TorConfig)]
197#[deftly(tor_config(no_default_trait))]
198#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
199#[cfg_attr(feature = "experimental-api", deftly(tor_config(vis = "pub")))]
200pub(crate) struct OpentelemetryFileExporterConfig {
201 #[deftly(tor_config(no_default))]
203 path: CfgPath,
204 #[deftly(tor_config(sub_builder))]
206 batch: OpentelemetryBatchConfig,
207}
208
209#[derive(Debug, Deftly, Copy, Clone, Eq, PartialEq, Serialize, Deserialize)]
213#[derive_deftly(TorConfig)]
214#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
215#[cfg_attr(feature = "experimental-api", deftly(tor_config(vis = "pub")))]
216pub(crate) struct OpentelemetryBatchConfig {
217 #[deftly(tor_config(default))]
219 max_queue_size: Option<usize>,
220 #[deftly(tor_config(default))]
222 max_export_batch_size: Option<usize>,
223 #[deftly(tor_config(no_magic, default))]
225 scheduled_delay: Option<Duration>,
226}
227
228#[cfg(feature = "opentelemetry")]
229impl From<OpentelemetryBatchConfig> for opentelemetry_sdk::trace::BatchConfig {
230 fn from(config: OpentelemetryBatchConfig) -> opentelemetry_sdk::trace::BatchConfig {
231 let batch_config = opentelemetry_sdk::trace::BatchConfigBuilder::default();
232
233 let batch_config = if let Some(max_queue_size) = config.max_queue_size {
234 batch_config.with_max_queue_size(max_queue_size)
235 } else {
236 batch_config
237 };
238
239 let batch_config = if let Some(max_export_batch_size) = config.max_export_batch_size {
240 batch_config.with_max_export_batch_size(max_export_batch_size)
241 } else {
242 batch_config
243 };
244
245 let batch_config = if let Some(scheduled_delay) = config.scheduled_delay {
246 batch_config.with_scheduled_delay(scheduled_delay)
247 } else {
248 batch_config
249 };
250
251 batch_config.build()
252 }
253}
254
255#[derive(Debug, Deftly, Copy, Clone, Eq, PartialEq, Serialize, Deserialize)]
257#[derive_deftly(TorConfig)]
258#[cfg(feature = "tokio-console")]
259#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
260#[cfg_attr(feature = "experimental-api", deftly(tor_config(vis = "pub")))]
261pub(crate) struct TokioConsoleConfig {
262 #[deftly(tor_config(default))]
267 enabled: bool,
268}
269
270#[cfg(not(feature = "tokio-console"))]
272type TokioConsoleConfig = ();
273
274fn filt_from_str_verbose(s: &str, source: &str) -> Result<Targets> {
279 Targets::from_str(s).with_context(|| format!("in {}", source))
280}
281
282fn filt_from_opt_str(s: &Option<String>, source: &str) -> Result<Option<Targets>> {
285 Ok(match s {
286 Some(s) if !s.is_empty() => Some(filt_from_str_verbose(s, source)?),
287 _ => None,
288 })
289}
290
291fn console_layer<S>(config: &LoggingConfig, cli: Option<&str>) -> Result<impl Layer<S> + use<S>>
293where
294 S: Subscriber + for<'span> tracing_subscriber::registry::LookupSpan<'span>,
295{
296 let timer = time::new_formatter(config.time_granularity);
297 let filter = cli
298 .map(|s| filt_from_str_verbose(s, "--log-level command line parameter"))
299 .or_else(|| filt_from_opt_str(&config.console, "logging.console").transpose())
300 .unwrap_or_else(|| Ok(Targets::from_str("debug").expect("bad default")))?;
301 let use_color = std::io::stderr().is_terminal();
302 Ok(fmt::Layer::default()
307 .fmt_fields(fields::ErrorsLastFieldFormatter)
309 .with_ansi(use_color)
310 .with_timer(timer)
311 .with_writer(std::io::stderr) .with_filter(filter))
313}
314
315#[cfg(feature = "journald")]
318fn journald_layer<S>(config: &LoggingConfig) -> Result<impl Layer<S>>
319where
320 S: Subscriber + for<'span> tracing_subscriber::registry::LookupSpan<'span>,
321{
322 if let Some(filter) = filt_from_opt_str(&config.journald, "logging.journald")? {
323 Ok(Some(tracing_journald::layer()?.with_filter(filter)))
324 } else {
325 Ok(None)
327 }
328}
329
330#[cfg(all(feature = "syslog", unix))]
333fn syslog_layer<S>(config: &LoggingConfig) -> Result<impl Layer<S>>
334where
335 S: Subscriber + for<'span> tracing_subscriber::registry::LookupSpan<'span>,
336{
337 use syslog_tracing::{Facility, Options, Syslog};
338
339 let identity = c"arti";
340
341 if let Some(filter) = filt_from_opt_str(&config.syslog, "logging.syslog")? {
342 let options = Options::LOG_PID;
343 let facility = Facility::Daemon;
344
345 let syslog_maker = Syslog::new(identity, options, facility).ok_or_else(|| {
346 anyhow::anyhow!("syslog already initialized; only one logger allowed")
347 })?;
348
349 let layer = tracing_subscriber::fmt::layer()
350 .with_writer(syslog_maker)
351 .with_ansi(false)
354 .without_time()
355 .with_filter(filter);
356
357 Ok(Some(layer))
358 } else {
359 Ok(None)
360 }
361}
362
363#[cfg(feature = "opentelemetry")]
368fn otel_layer<S>(config: &LoggingConfig, path_resolver: &CfgPathResolver) -> Result<impl Layer<S>>
369where
370 S: Subscriber + for<'span> tracing_subscriber::registry::LookupSpan<'span>,
371{
372 use opentelemetry::trace::TracerProvider;
373 use opentelemetry_otlp::WithExportConfig;
374
375 if config.opentelemetry.file.is_some() && config.opentelemetry.http.is_some() {
376 return Err(ConfigBuildError::Invalid {
377 field: "logging.opentelemetry".into(),
378 problem: "Only one OpenTelemetry exporter can be enabled at once.".into(),
379 }
380 .into());
381 }
382
383 let resource = opentelemetry_sdk::Resource::builder()
384 .with_service_name("arti")
385 .build();
386
387 let span_processor = if let Some(otel_file_config) = &config.opentelemetry.file {
388 let file = std::fs::File::options()
389 .create(true)
390 .append(true)
391 .open(otel_file_config.path.path(path_resolver)?)?;
392
393 let exporter = otlp_file_exporter::FileExporter::new(file, resource.clone());
394
395 opentelemetry_sdk::trace::BatchSpanProcessor::builder(exporter)
396 .with_batch_config(otel_file_config.batch.into())
397 .build()
398 } else if let Some(otel_http_config) = &config.opentelemetry.http {
399 if otel_http_config.endpoint.starts_with("http://")
400 && !(otel_http_config.endpoint.starts_with("http://localhost")
401 || otel_http_config.endpoint.starts_with("http://127.0.0.1"))
402 {
403 return Err(ConfigBuildError::Invalid {
404 field: "logging.opentelemetry.http.endpoint".into(),
405 problem: "OpenTelemetry endpoint is set to HTTP on a non-localhost address! For security reasons, this is not supported.".into(),
406 }
407 .into());
408 }
409 let exporter = opentelemetry_otlp::SpanExporter::builder()
410 .with_http()
411 .with_endpoint(otel_http_config.endpoint.clone());
412
413 let exporter = if let Some(timeout) = otel_http_config.timeout {
414 exporter.with_timeout(timeout)
415 } else {
416 exporter
417 };
418
419 let exporter = exporter.build()?;
420
421 opentelemetry_sdk::trace::BatchSpanProcessor::builder(exporter)
422 .with_batch_config(otel_http_config.batch.into())
423 .build()
424 } else {
425 return Ok(None);
426 };
427
428 let tracer_provider = opentelemetry_sdk::trace::SdkTracerProvider::builder()
429 .with_resource(resource.clone())
430 .with_span_processor(span_processor)
431 .build();
432
433 let tracer = tracer_provider.tracer("otel_file_tracer");
434
435 Ok(Some(tracing_opentelemetry::layer().with_tracer(tracer)))
436}
437
438fn logfile_layer<S>(
444 config: &LogfileConfig,
445 granularity: std::time::Duration,
446 mistrust: &Mistrust,
447 path_resolver: &CfgPathResolver,
448) -> Result<(impl Layer<S> + Send + Sync + Sized + use<S>, WorkerGuard)>
449where
450 S: Subscriber + for<'span> tracing_subscriber::registry::LookupSpan<'span> + Send + Sync,
451{
452 use tracing_appender::{
453 non_blocking,
454 rolling::{RollingFileAppender, Rotation},
455 };
456 let timer = time::new_formatter(granularity);
457
458 let filter = filt_from_str_verbose(&config.filter, "logging.files.filter")?;
459 let rotation = match config.rotate {
460 LogRotation::Daily => Rotation::DAILY,
461 LogRotation::Hourly => Rotation::HOURLY,
462 _ => Rotation::NEVER,
463 };
464 let path = config.path.path(path_resolver)?;
465
466 let directory = match path.parent() {
467 None => {
468 return Err(anyhow!(
469 "Logfile path \"{}\" did not have a parent directory",
470 path.display_lossy()
471 ));
472 }
473 Some(p) if p == Path::new("") => Path::new("."),
474 Some(d) => d,
475 };
476 mistrust.make_directory(directory).with_context(|| {
477 format!(
478 "Unable to create parent directory for logfile \"{}\"",
479 path.display_lossy()
480 )
481 })?;
482 let fname = path
483 .file_name()
484 .ok_or_else(|| anyhow!("No path for log file"))
485 .map(Path::new)?;
486
487 let appender = RollingFileAppender::new(rotation, directory, fname);
488 let (nonblocking, guard) = non_blocking(appender);
489 let layer = fmt::layer()
490 .fmt_fields(fields::ErrorsLastFieldFormatter)
492 .with_ansi(false)
493 .with_writer(nonblocking)
494 .with_timer(timer)
495 .with_filter(filter);
496 Ok((layer, guard))
497}
498
499fn logfile_layers<S>(
504 config: &LoggingConfig,
505 mistrust: &Mistrust,
506 path_resolver: &CfgPathResolver,
507) -> Result<(impl Layer<S> + use<S>, Vec<WorkerGuard>)>
508where
509 S: Subscriber + for<'span> tracing_subscriber::registry::LookupSpan<'span> + Send + Sync,
510{
511 let mut guards = Vec::new();
512 if config.files.is_empty() {
513 return Ok((None, guards));
516 }
517
518 let (layer, guard) = logfile_layer(
519 &config.files[0],
520 config.time_granularity,
521 mistrust,
522 path_resolver,
523 )?;
524 guards.push(guard);
525
526 let mut layer: Box<dyn Layer<S> + Send + Sync + 'static> = Box::new(layer);
529
530 for logfile in &config.files[1..] {
531 let (new_layer, guard) =
532 logfile_layer(logfile, config.time_granularity, mistrust, path_resolver)?;
533 layer = Box::new(layer.and_then(new_layer));
534 guards.push(guard);
535 }
536
537 Ok((Some(layer), guards))
538}
539
540fn install_panic_handler() {
543 let default_handler = std::panic::take_hook();
549 std::panic::set_hook(Box::new(move |panic_info| {
550 default_handler(panic_info);
553
554 let msg = match panic_info.payload().downcast_ref::<&'static str>() {
556 Some(s) => *s,
557 None => match panic_info.payload().downcast_ref::<String>() {
558 Some(s) => &s[..],
559 None => "Box<dyn Any>",
560 },
561 };
562
563 let backtrace = std::backtrace::Backtrace::force_capture();
564 match panic_info.location() {
565 Some(location) => error!("Panic at {}: {}\n{}", location, msg, backtrace),
566 None => error!("Panic at ???: {}\n{}", msg, backtrace),
567 };
568 }));
569}
570
571#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
574pub(crate) struct LogGuards {
575 #[allow(unused)]
577 guards: Vec<WorkerGuard>,
578
579 #[allow(unused)]
581 safelog_guard: Option<safelog::Guard>,
582}
583
584#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
589#[cfg_attr(docsrs, doc(cfg(feature = "experimental-api")))]
590pub(crate) fn setup_logging(
591 config: &LoggingConfig,
592 mistrust: &Mistrust,
593 path_resolver: &CfgPathResolver,
594 cli: Option<&str>,
595) -> Result<LogGuards> {
596 let registry = registry().with(console_layer(config, cli)?);
605
606 #[cfg(feature = "journald")]
607 let registry = registry.with(journald_layer(config)?);
608
609 #[cfg(all(feature = "syslog", unix))]
610 let registry = registry.with(syslog_layer(config)?);
611
612 #[cfg(feature = "opentelemetry")]
613 let registry = registry.with(otel_layer(config, path_resolver)?);
614
615 #[cfg(feature = "tokio-console")]
616 let registry = {
617 let tokio_layer = if config.tokio_console.enabled {
624 Some(console_subscriber::spawn())
625 } else {
626 None
627 };
628 registry.with(tokio_layer)
629 };
630
631 let (layer, guards) = logfile_layers(config, mistrust, path_resolver)?;
632 let registry = registry.with(layer);
633
634 registry.init();
635
636 let safelog_guard = if config.log_sensitive_information {
637 match safelog::disable_safe_logging() {
638 Ok(guard) => Some(guard),
639 Err(e) => {
640 warn_report!(e, "Unable to disable safe logging");
643 None
644 }
645 }
646 } else {
647 None
648 };
649
650 let mode = if config.protocol_warnings {
651 tor_error::tracing::ProtocolWarningMode::Warn
652 } else {
653 tor_error::tracing::ProtocolWarningMode::Off
654 };
655 tor_error::tracing::set_protocol_warning_mode(mode);
656
657 install_panic_handler();
658
659 Ok(LogGuards {
660 guards,
661 safelog_guard,
662 })
663}