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(
52 sub_builder,
53 cfg = r#" feature = "opentelemetry" "#,
54 cfg_desc = "with opentelemetry support"
55 ))]
56 opentelemetry: OpentelemetryConfig,
57
58 #[deftly(tor_config(
60 sub_builder,
61 cfg = r#" feature = "tokio-console" "#,
62 cfg_desc = "with tokio-console support"
63 ))]
64 tokio_console: TokioConsoleConfig,
65
66 #[deftly(tor_config(list(element(build), listtype = "LogfileList"), default = "vec![]"))]
70 files: Vec<LogfileConfig>,
71
72 #[deftly(tor_config(default))]
84 log_sensitive_information: bool,
85
86 #[deftly(tor_config(default))]
88 protocol_warnings: bool,
89
90 #[deftly(tor_config(default = "std::time::Duration::new(1,0)"))]
100 time_granularity: std::time::Duration,
101}
102
103#[allow(clippy::unnecessary_wraps)]
105fn default_console_filter() -> Option<String> {
106 Some("info".to_owned())
107}
108
109#[derive(Debug, Deftly, Clone, Eq, PartialEq)]
111#[derive_deftly(TorConfig)]
112#[deftly(tor_config(no_default_trait))]
113#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
114#[cfg_attr(feature = "experimental-api", deftly(tor_config(vis = "pub")))]
115pub(crate) struct LogfileConfig {
116 #[deftly(tor_config(default))]
118 rotate: LogRotation,
119 #[deftly(tor_config(no_default))]
121 path: CfgPath,
122 #[deftly(tor_config(no_default))]
124 filter: String,
125}
126
127#[derive(Debug, Default, Clone, Serialize, Deserialize, Copy, Eq, PartialEq)]
129#[non_exhaustive]
130#[serde(rename_all = "lowercase")]
131#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
132pub(crate) enum LogRotation {
133 Daily,
135 Hourly,
137 #[default]
139 Never,
140}
141
142#[derive(Debug, Deftly, Clone, Eq, PartialEq, Serialize, Deserialize)]
144#[derive_deftly(TorConfig)]
145#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
146#[cfg_attr(feature = "experimental-api", deftly(tor_config(vis = "pub")))]
147pub(crate) struct OpentelemetryConfig {
148 #[deftly(tor_config(default))]
150 file: Option<OpentelemetryFileExporterConfig>,
151 #[deftly(tor_config(default))]
153 http: Option<OpentelemetryHttpExporterConfig>,
154}
155
156#[derive(Debug, Deftly, Clone, Eq, PartialEq, Serialize, Deserialize)]
158#[derive_deftly(TorConfig)]
159#[deftly(tor_config(no_default_trait))]
160#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
161#[cfg_attr(feature = "experimental-api", deftly(tor_config(vis = "pub")))]
162pub(crate) struct OpentelemetryHttpExporterConfig {
163 #[deftly(tor_config(no_default))]
167 endpoint: String,
168 #[deftly(tor_config(sub_builder))]
170 batch: OpentelemetryBatchConfig,
171 #[deftly(tor_config(no_magic, default))]
179 timeout: Option<Duration>,
180 }
183
184#[derive(Debug, Deftly, Clone, Eq, PartialEq, Serialize, Deserialize)]
186#[derive_deftly(TorConfig)]
187#[deftly(tor_config(no_default_trait))]
188#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
189#[cfg_attr(feature = "experimental-api", deftly(tor_config(vis = "pub")))]
190pub(crate) struct OpentelemetryFileExporterConfig {
191 #[deftly(tor_config(no_default))]
193 path: CfgPath,
194 #[deftly(tor_config(sub_builder))]
196 batch: OpentelemetryBatchConfig,
197}
198
199#[derive(Debug, Deftly, Copy, Clone, Eq, PartialEq, Serialize, Deserialize)]
203#[derive_deftly(TorConfig)]
204#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
205#[cfg_attr(feature = "experimental-api", deftly(tor_config(vis = "pub")))]
206pub(crate) struct OpentelemetryBatchConfig {
207 #[deftly(tor_config(default))]
209 max_queue_size: Option<usize>,
210 #[deftly(tor_config(default))]
212 max_export_batch_size: Option<usize>,
213 #[deftly(tor_config(no_magic, default))]
215 scheduled_delay: Option<Duration>,
216}
217
218#[cfg(feature = "opentelemetry")]
219impl From<OpentelemetryBatchConfig> for opentelemetry_sdk::trace::BatchConfig {
220 fn from(config: OpentelemetryBatchConfig) -> opentelemetry_sdk::trace::BatchConfig {
221 let batch_config = opentelemetry_sdk::trace::BatchConfigBuilder::default();
222
223 let batch_config = if let Some(max_queue_size) = config.max_queue_size {
224 batch_config.with_max_queue_size(max_queue_size)
225 } else {
226 batch_config
227 };
228
229 let batch_config = if let Some(max_export_batch_size) = config.max_export_batch_size {
230 batch_config.with_max_export_batch_size(max_export_batch_size)
231 } else {
232 batch_config
233 };
234
235 let batch_config = if let Some(scheduled_delay) = config.scheduled_delay {
236 batch_config.with_scheduled_delay(scheduled_delay)
237 } else {
238 batch_config
239 };
240
241 batch_config.build()
242 }
243}
244
245#[derive(Debug, Deftly, Copy, Clone, Eq, PartialEq, Serialize, Deserialize)]
247#[derive_deftly(TorConfig)]
248#[cfg(feature = "tokio-console")]
249#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
250#[cfg_attr(feature = "experimental-api", deftly(tor_config(vis = "pub")))]
251pub(crate) struct TokioConsoleConfig {
252 #[deftly(tor_config(default))]
257 enabled: bool,
258}
259
260#[cfg(not(feature = "tokio-console"))]
262type TokioConsoleConfig = ();
263
264fn filt_from_str_verbose(s: &str, source: &str) -> Result<Targets> {
269 Targets::from_str(s).with_context(|| format!("in {}", source))
270}
271
272fn filt_from_opt_str(s: &Option<String>, source: &str) -> Result<Option<Targets>> {
275 Ok(match s {
276 Some(s) if !s.is_empty() => Some(filt_from_str_verbose(s, source)?),
277 _ => None,
278 })
279}
280
281fn console_layer<S>(config: &LoggingConfig, cli: Option<&str>) -> Result<impl Layer<S> + use<S>>
283where
284 S: Subscriber + for<'span> tracing_subscriber::registry::LookupSpan<'span>,
285{
286 let timer = time::new_formatter(config.time_granularity);
287 let filter = cli
288 .map(|s| filt_from_str_verbose(s, "--log-level command line parameter"))
289 .or_else(|| filt_from_opt_str(&config.console, "logging.console").transpose())
290 .unwrap_or_else(|| Ok(Targets::from_str("debug").expect("bad default")))?;
291 let use_color = std::io::stderr().is_terminal();
292 Ok(fmt::Layer::default()
297 .fmt_fields(fields::ErrorsLastFieldFormatter)
299 .with_ansi(use_color)
300 .with_timer(timer)
301 .with_writer(std::io::stderr) .with_filter(filter))
303}
304
305#[cfg(feature = "journald")]
308fn journald_layer<S>(config: &LoggingConfig) -> Result<impl Layer<S>>
309where
310 S: Subscriber + for<'span> tracing_subscriber::registry::LookupSpan<'span>,
311{
312 if let Some(filter) = filt_from_opt_str(&config.journald, "logging.journald")? {
313 Ok(Some(tracing_journald::layer()?.with_filter(filter)))
314 } else {
315 Ok(None)
317 }
318}
319
320#[cfg(feature = "opentelemetry")]
325fn otel_layer<S>(config: &LoggingConfig, path_resolver: &CfgPathResolver) -> Result<impl Layer<S>>
326where
327 S: Subscriber + for<'span> tracing_subscriber::registry::LookupSpan<'span>,
328{
329 use opentelemetry::trace::TracerProvider;
330 use opentelemetry_otlp::WithExportConfig;
331
332 if config.opentelemetry.file.is_some() && config.opentelemetry.http.is_some() {
333 return Err(ConfigBuildError::Invalid {
334 field: "logging.opentelemetry".into(),
335 problem: "Only one OpenTelemetry exporter can be enabled at once.".into(),
336 }
337 .into());
338 }
339
340 let resource = opentelemetry_sdk::Resource::builder()
341 .with_service_name("arti")
342 .build();
343
344 let span_processor = if let Some(otel_file_config) = &config.opentelemetry.file {
345 let file = std::fs::File::options()
346 .create(true)
347 .append(true)
348 .open(otel_file_config.path.path(path_resolver)?)?;
349
350 let exporter = otlp_file_exporter::FileExporter::new(file, resource.clone());
351
352 opentelemetry_sdk::trace::BatchSpanProcessor::builder(exporter)
353 .with_batch_config(otel_file_config.batch.into())
354 .build()
355 } else if let Some(otel_http_config) = &config.opentelemetry.http {
356 if otel_http_config.endpoint.starts_with("http://")
357 && !(otel_http_config.endpoint.starts_with("http://localhost")
358 || otel_http_config.endpoint.starts_with("http://127.0.0.1"))
359 {
360 return Err(ConfigBuildError::Invalid {
361 field: "logging.opentelemetry.http.endpoint".into(),
362 problem: "OpenTelemetry endpoint is set to HTTP on a non-localhost address! For security reasons, this is not supported.".into(),
363 }
364 .into());
365 }
366 let exporter = opentelemetry_otlp::SpanExporter::builder()
367 .with_http()
368 .with_endpoint(otel_http_config.endpoint.clone());
369
370 let exporter = if let Some(timeout) = otel_http_config.timeout {
371 exporter.with_timeout(timeout)
372 } else {
373 exporter
374 };
375
376 let exporter = exporter.build()?;
377
378 opentelemetry_sdk::trace::BatchSpanProcessor::builder(exporter)
379 .with_batch_config(otel_http_config.batch.into())
380 .build()
381 } else {
382 return Ok(None);
383 };
384
385 let tracer_provider = opentelemetry_sdk::trace::SdkTracerProvider::builder()
386 .with_resource(resource.clone())
387 .with_span_processor(span_processor)
388 .build();
389
390 let tracer = tracer_provider.tracer("otel_file_tracer");
391
392 Ok(Some(tracing_opentelemetry::layer().with_tracer(tracer)))
393}
394
395fn logfile_layer<S>(
401 config: &LogfileConfig,
402 granularity: std::time::Duration,
403 mistrust: &Mistrust,
404 path_resolver: &CfgPathResolver,
405) -> Result<(impl Layer<S> + Send + Sync + Sized + use<S>, WorkerGuard)>
406where
407 S: Subscriber + for<'span> tracing_subscriber::registry::LookupSpan<'span> + Send + Sync,
408{
409 use tracing_appender::{
410 non_blocking,
411 rolling::{RollingFileAppender, Rotation},
412 };
413 let timer = time::new_formatter(granularity);
414
415 let filter = filt_from_str_verbose(&config.filter, "logging.files.filter")?;
416 let rotation = match config.rotate {
417 LogRotation::Daily => Rotation::DAILY,
418 LogRotation::Hourly => Rotation::HOURLY,
419 _ => Rotation::NEVER,
420 };
421 let path = config.path.path(path_resolver)?;
422
423 let directory = match path.parent() {
424 None => {
425 return Err(anyhow!(
426 "Logfile path \"{}\" did not have a parent directory",
427 path.display_lossy()
428 ));
429 }
430 Some(p) if p == Path::new("") => Path::new("."),
431 Some(d) => d,
432 };
433 mistrust.make_directory(directory).with_context(|| {
434 format!(
435 "Unable to create parent directory for logfile \"{}\"",
436 path.display_lossy()
437 )
438 })?;
439 let fname = path
440 .file_name()
441 .ok_or_else(|| anyhow!("No path for log file"))
442 .map(Path::new)?;
443
444 let appender = RollingFileAppender::new(rotation, directory, fname);
445 let (nonblocking, guard) = non_blocking(appender);
446 let layer = fmt::layer()
447 .fmt_fields(fields::ErrorsLastFieldFormatter)
449 .with_ansi(false)
450 .with_writer(nonblocking)
451 .with_timer(timer)
452 .with_filter(filter);
453 Ok((layer, guard))
454}
455
456fn logfile_layers<S>(
461 config: &LoggingConfig,
462 mistrust: &Mistrust,
463 path_resolver: &CfgPathResolver,
464) -> Result<(impl Layer<S> + use<S>, Vec<WorkerGuard>)>
465where
466 S: Subscriber + for<'span> tracing_subscriber::registry::LookupSpan<'span> + Send + Sync,
467{
468 let mut guards = Vec::new();
469 if config.files.is_empty() {
470 return Ok((None, guards));
473 }
474
475 let (layer, guard) = logfile_layer(
476 &config.files[0],
477 config.time_granularity,
478 mistrust,
479 path_resolver,
480 )?;
481 guards.push(guard);
482
483 let mut layer: Box<dyn Layer<S> + Send + Sync + 'static> = Box::new(layer);
486
487 for logfile in &config.files[1..] {
488 let (new_layer, guard) =
489 logfile_layer(logfile, config.time_granularity, mistrust, path_resolver)?;
490 layer = Box::new(layer.and_then(new_layer));
491 guards.push(guard);
492 }
493
494 Ok((Some(layer), guards))
495}
496
497fn install_panic_handler() {
500 let default_handler = std::panic::take_hook();
506 std::panic::set_hook(Box::new(move |panic_info| {
507 default_handler(panic_info);
510
511 let msg = match panic_info.payload().downcast_ref::<&'static str>() {
513 Some(s) => *s,
514 None => match panic_info.payload().downcast_ref::<String>() {
515 Some(s) => &s[..],
516 None => "Box<dyn Any>",
517 },
518 };
519
520 let backtrace = std::backtrace::Backtrace::force_capture();
521 match panic_info.location() {
522 Some(location) => error!("Panic at {}: {}\n{}", location, msg, backtrace),
523 None => error!("Panic at ???: {}\n{}", msg, backtrace),
524 };
525 }));
526}
527
528#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
531pub(crate) struct LogGuards {
532 #[allow(unused)]
534 guards: Vec<WorkerGuard>,
535
536 #[allow(unused)]
538 safelog_guard: Option<safelog::Guard>,
539}
540
541#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
546#[cfg_attr(docsrs, doc(cfg(feature = "experimental-api")))]
547pub(crate) fn setup_logging(
548 config: &LoggingConfig,
549 mistrust: &Mistrust,
550 path_resolver: &CfgPathResolver,
551 cli: Option<&str>,
552) -> Result<LogGuards> {
553 let registry = registry().with(console_layer(config, cli)?);
562
563 #[cfg(feature = "journald")]
564 let registry = registry.with(journald_layer(config)?);
565
566 #[cfg(feature = "opentelemetry")]
567 let registry = registry.with(otel_layer(config, path_resolver)?);
568
569 #[cfg(feature = "tokio-console")]
570 let registry = {
571 let tokio_layer = if config.tokio_console.enabled {
578 Some(console_subscriber::spawn())
579 } else {
580 None
581 };
582 registry.with(tokio_layer)
583 };
584
585 let (layer, guards) = logfile_layers(config, mistrust, path_resolver)?;
586 let registry = registry.with(layer);
587
588 registry.init();
589
590 let safelog_guard = if config.log_sensitive_information {
591 match safelog::disable_safe_logging() {
592 Ok(guard) => Some(guard),
593 Err(e) => {
594 warn_report!(e, "Unable to disable safe logging");
597 None
598 }
599 }
600 } else {
601 None
602 };
603
604 let mode = if config.protocol_warnings {
605 tor_error::tracing::ProtocolWarningMode::Warn
606 } else {
607 tor_error::tracing::ProtocolWarningMode::Off
608 };
609 tor_error::tracing::set_protocol_warning_mode(mode);
610
611 install_panic_handler();
612
613 Ok(LogGuards {
614 guards,
615 safelog_guard,
616 })
617}